zkSync Era - Sathish9098's results

Future-proof zkEVM on the mission to scale freedom for all.

General Information

Platform: Code4rena

Start Date: 02/10/2023

Pot Size: $1,100,000 USDC

Total HM: 28

Participants: 64

Period: 21 days

Judge: GalloDaSballo

Total Solo HM: 13

Id: 292

League: ETH

zkSync

Findings Distribution

Researcher Performance

Rank: 33/64

Findings: 2

Award: $990.53

Gas:
grade-b
Analysis:
grade-b

๐ŸŒŸ Selected for report: 0

๐Ÿš€ Solo Findings: 0

Findings Information

๐ŸŒŸ Selected for report: lsaudit

Also found by: 0xAnah, 0xhacksmithh, 0xsomeone, SM3_SS, Sathish9098, albahaca, c3phas, hunter_w3b

Labels

bug
G (Gas Optimization)
grade-b
sufficient quality report
edited-by-warden
G-09

Awards

213.7261 USDC - $213.73

External Links

GAS OPTIMIZATIONS

## Issues

[G-1] Improving storage efficiency through Struct Packing

Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings.

[G-1.1] protocolVersion and l2SystemContractsUpgradeBatchNumber can be uint96 instead of uint256 : Saves 4000 GAS , 2 SLOT

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/Storage.sol#L136

The protocolVersion value is increased in a sequential way, meaning that the next version number is always one greater than the current version number. This means that the protocolVersion value cannot exceed the maximum range of uint96 unless the protocol is upgraded more than 2^96 times.

2^96 is a very large number, and it is unlikely that any protocol would ever need to be upgraded more than this many times. For example, the Ethereum protocol has only been upgraded a handful of times since it was launched in 2015.

Therefore, it is safe to change the protocolVersion variable to uint96 without worrying about the value overflowing. This will save 2000 gas and 1 slot, which can be significant for contracts that are frequently upgraded.

This variable, l2SystemContractsUpgradeBatchNumber, represents the batch number of the latest upgrade transaction. If its value is 0, it signifies that no upgrade transactions have occurred. The use of uint96 is more than adequate for this context.

FILE: Breadcrumbs2023-10-zksync/code/contracts/ethereum/contracts/zksync/Storage.sol


struct AppStorage {
    /// @dev Storage of variables needed for deprecated diamond cut facet
    uint256[7] __DEPRECATED_diamondCutStorage;
    /// @notice Address which will exercise critical changes to the Diamond Proxy (upgrades, freezing & unfreezing)
    address governor;
    /// @notice Address that the governor proposed as one that will replace it
    address pendingGovernor;
    /// @notice List of permitted validators
    mapping(address => bool) validators;
    /// @dev Verifier contract. Used to verify aggregated proof for batches
    IVerifier verifier;
    /// @notice Total number of executed batches i.e. batches[totalBatchesExecuted] points at the latest executed batch
    /// (batch 0 is genesis)
    uint256 totalBatchesExecuted;
    /// @notice Total number of proved batches i.e. batches[totalBatchesProved] points at the latest proved batch
    uint256 totalBatchesVerified;
    /// @notice Total number of committed batches i.e. batches[totalBatchesCommitted] points at the latest committed
    /// batch
    uint256 totalBatchesCommitted;
    /// @dev Stored hashed StoredBatch for batch number
    mapping(uint256 => bytes32) storedBatchHashes;
    /// @dev Stored root hashes of L2 -> L1 logs
    mapping(uint256 => bytes32) l2LogsRootHashes;
    /// @dev Container that stores transactions requested from L1
    PriorityQueue.Queue priorityQueue;
    /// @dev The smart contract that manages the list with permission to call contract functions
    IAllowList allowList;
    /// @notice Part of the configuration parameters of ZKP circuits. Used as an input for the verifier smart contract
    VerifierParams verifierParams;
    /// @notice Bytecode hash of bootloader program.
    /// @dev Used as an input to zkp-circuit.
    bytes32 l2BootloaderBytecodeHash;
    /// @notice Bytecode hash of default account (bytecode for EOA).
    /// @dev Used as an input to zkp-circuit.
    bytes32 l2DefaultAccountBytecodeHash;
    /// @dev Indicates that the porter may be touched on L2 transactions.
    /// @dev Used as an input to zkp-circuit.
    bool zkPorterIsAvailable;
    /// @dev The maximum number of the L2 gas that a user can request for L1 -> L2 transactions
    /// @dev This is the maximum number of L2 gas that is available for the "body" of the transaction, i.e.
    /// without overhead for proving the batch.
    uint256 priorityTxMaxGasLimit;
    /// @dev Storage of variables needed for upgrade facet
    UpgradeStorage __DEPRECATED_upgrades;
    /// @dev A mapping L2 batch number => message number => flag.
    /// @dev The L2 -> L1 log is sent for every withdrawal, so this mapping is serving as
    /// a flag to indicate that the message was already processed.
    /// @dev Used to indicate that eth withdrawal was already processed
    mapping(uint256 => mapping(uint256 => bool)) isEthWithdrawalFinalized;
    /// @dev The most recent withdrawal time and amount reset
    uint256 __DEPRECATED_lastWithdrawalLimitReset;
    /// @dev The accumulated withdrawn amount during the withdrawal limit window
    uint256 __DEPRECATED_withdrawnAmountInWindow;
    /// @dev A mapping user address => the total deposited amount by the user
    mapping(address => uint256) totalDepositedAmountPerUser;
    /// @dev Stores the protocol version. Note, that the protocol version may not only encompass changes to the
    /// smart contracts, but also to the node behavior.
-    uint256 protocolVersion;
    /// @dev Hash of the system contract upgrade transaction. If 0, then no upgrade transaction needs to be done.
    bytes32 l2SystemContractsUpgradeTxHash;
    /// @dev Batch number where the upgrade transaction has happened. If 0, then no upgrade transaction has happened
    /// yet.
-    uint256 l2SystemContractsUpgradeBatchNumber;
+    uint96 l2SystemContractsUpgradeBatchNumber;
    /// @dev Address which will exercise non-critical changes to the Diamond Proxy (changing validator set & unfreezing)
    address admin;
    /// @notice Address that the governor or admin proposed as one that will replace admin role
    address pendingAdmin;
+    uint96 protocolVersion;
}

[G-1.2] newProtocolVersion can be uint96 instead of uint256 : Saves 2000 GAS , 1 SLOT

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol#L31-L43

The newprotocolVersion value is increased in a sequential way, meaning that the next version number is always one greater than the current version number. This means that the newprotocolVersion value cannot exceed the maximum range of uint96 unless the protocol is upgraded more than 2^96 times.

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol

struct ProposedUpgrade {
        IMailbox.L2CanonicalTransaction l2ProtocolUpgradeTx;
        bytes[] factoryDeps;
        bytes32 bootloaderHash;
        bytes32 defaultAccountHash;
        address verifier;
+         uint96 newProtocolVersion;
        VerifierParams verifierParams;
        bytes l1ContractsUpgradeCalldata;
        bytes postUpgradeCalldata;
        uint256 upgradeTimestamp;
-         uint256 newProtocolVersion;
        address newAllowList;
    }

[G-1.3] txType and nonce can be uint128 instead of uint256 : Saves 2000 GAS , 1 SLOT

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/interfaces/IMailbox.sol#L35-L62

The change to uint128 for txType and nonce is safe because the maximum values of these fields are within the range of uint128.

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/interfaces/IMailbox.sol

 struct L2CanonicalTransaction {
-        uint256 txType;
+        uint128 txType;
+        uint128 nonce;
        uint256 from;
        uint256 to;
        uint256 gasLimit;
        uint256 gasPerPubdataByteLimit;
        uint256 maxFeePerGas;
        uint256 maxPriorityFeePerGas;
        uint256 paymaster;
-        uint256 nonce;
        uint256 value;
        // In the future, we might want to add some
        // new fields to the struct. The `txData` struct
        // is to be passed to account and any changes to its structure
        // would mean a breaking change to these accounts. To prevent this,
        // we should keep some fields as "reserved".
        // It is also recommended that their length is fixed, since
        // it would allow easier proof integration (in case we will need
        // some special circuit for preprocessing transactions).
        uint256[4] reserved;
        bytes data;
        bytes signature;
        uint256[] factoryDeps;
        bytes paymasterInput;
        // Reserved dynamic type for the future use-case. Using it should be avoided,
        // But it is still here, just in case we want to enable some additional functionality.
        bytes reservedDynamic;
    }

[G-2] Replacing booleans with uint256(1)/uint256(2) for gas optimization

Saves 100000 GAS from 5 Instances

Avoids a Gsset (20000 gas) when changing from false to true, after having been true in the past. Since most of the bools aren't changed twice in one transaction, I've counted the amount of gas as half of the full amount, for each variable.

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/Storage.sol

87: mapping(address => bool) validators;

116: bool zkPorterIsAvailable;

127: mapping(uint256 => mapping(uint256 => bool)) isEthWithdrawalFinalized;

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/Storage.sol#L87

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol

32: bool isFreezable;

53: bool isFrozen;

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol#L32

[G-3] Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate

Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.

FILE: Breadcrumbs2023-10-zksync/code/contracts/ethereum/contracts/zksync/Storage.sol

 /// @dev Stored hashed StoredBatch for batch number
99:  mapping(uint256 => bytes32) storedBatchHashes;
100:    /// @dev Stored root hashes of L2 -> L1 logs
101: mapping(uint256 => bytes32) l2LogsRootHashes;

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/Storage.sol#L99-L101

## [G-4] ``totalDepositedAmountPerUser[_l1Token][_depositor]`` should be cached

The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.

totalDepositedAmountPerUser[_l1Token][_depositor] should be cached : Saves 100 GAS , 1 SLOD

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L346-L348

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol

346: } else {
+    uint256 totalDepositedAmountPerUserL1TokenDepositor = totalDepositedAmountPerUser[_l1Token][_depositor] ;
- 347:            require(totalDepositedAmountPerUser[_l1Token][_depositor] + _amount <= limitData.depositCap, "d1");
+ 347:            require(totalDepositedAmountPerUserL1TokenDepositor + _amount <= limitData.depositCap, "d1");
- 348:            totalDepositedAmountPerUser[_l1Token][_depositor] += _amount;
+ 348:            totalDepositedAmountPerUser[_l1Token][_depositor] = totalDepositedAmountPerUserL1TokenDepositor + _amount;
349:        }

[G-5] Efficient event consolidation for Glogtopic Gas Savings

We can combine the events into one singular event to save two Glogtopic (375 gas) that would otherwise be paid for the additional events. Saves 750 GAS

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Admin.sol

- 37: emit NewPendingGovernor(pendingGovernor, address(0));
- 38: emit NewGovernor(previousGovernor, pendingGovernor);

+ PendingAndNewGovernor(pendingGovernor, address(0),previousGovernor,pendingGovernor);


- 61: emit NewPendingAdmin(pendingAdmin, address(0));
- 62: emit NewAdmin(previousAdmin, pendingAdmin);

+ PendingAndNewAdmin(pendingAdmin, address(0),previousAdmin,pendingAdmin);

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L37-L38 ##

[G-6] Optimize gas usage by avoiding variable declarations inside loops

The variables action, facet, isFacetFreezable, and selectors are all declared inside the loop. This means that a new instance of each variable will be created for each iteration of the loop. Saves 200-300 Gas per iteration


for (uint256 i = 0; i < facetCutsLength; i = i.uncheckedInc()) {
            action = facetCuts[i].action;
             facet = facetCuts[i].facet;
            isFacetFreezable = facetCuts[i].isFreezable;
             selectors = facetCuts[i].selectors;

            require(selectors.length > 0, "B"); // no functions for diamond cut

            if (action == Action.Add) {
                _addFunctions(facet, selectors, isFacetFreezable);
            } else if (action == Action.Replace) {
                _replaceFunctions(facet, selectors, isFacetFreezable);
            } else if (action == Action.Remove) {
                _removeFunctions(facet, selectors);
            } else {
                revert("C"); // undefined diamond cut action
            }
        }

diff --git a/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol b/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol
index 0b1fbdc..2f3cf9e 100644
--- a/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol
+++ b/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol
@@ -97,11 +97,15 @@ library Diamond {
         address initAddress = _diamondCut.initAddress;
         bytes memory initCalldata = _diamondCut.initCalldata;
         uint256 facetCutsLength = facetCuts.length;
+        Action action;
+        address facet;
+      bool isFacetFreezable;
+    bytes4[] memory selectors;
         for (uint256 i = 0; i < facetCutsLength; i = i.uncheckedInc()) {
-            Action action = facetCuts[i].action;
+            action = facetCuts[i].action;
-            address facet = facetCuts[i].facet;
+             facet = facetCuts[i].facet;
-            bool isFacetFreezable = facetCuts[i].isFreezable;
+            isFacetFreezable = facetCuts[i].isFreezable;
-            bytes4[] memory selectors = facetCuts[i].selectors;
+            selectors = facetCuts[i].selectors;

Saves 13 GAS

FILE: Breadcrumbs2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol

+    bytes32 hashedBytecode ;
 for (uint256 i = 0; i < factoryDepsLen; i = i.uncheckedInc()) {
-            bytes32 hashedBytecode = L2ContractHelper.hashL2Bytecode(_factoryDeps[i]);
+            hashedBytecode = L2ContractHelper.hashL2Bytecode(_factoryDeps[i]);

            // Store the resulting hash sequentially in bytes.
            assembly {
                mstore(add(hashedFactoryDeps, mul(add(i, 1), 32)), hashedBytecode)
            }

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L396-L401

[G-7] Use dot notation method for struct assignment

We have a few methods we can use when assigning values to struct

  • Type({field1: value1, field2: value2});
  • Type(value1,value2);
  • dot notation(structObject.field = value1);

Saves 135 GAS

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol#L223-L227

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol

- ds.selectorToFacet[_selector] = SelectorToFacet({
-            facetAddress: _facet,
-            selectorPosition: selectorPosition,
-            isFreezable: _isSelectorFreezable
-        });

+ ds.selectorToFacet[_selector].facetAddress = _facet ;
+ ds.selectorToFacet[_selector].selectorPosition = selectorPosition ;
+ ds.selectorToFacet[_selector].isFreezable= _isSelectorFreezable ;

[G-8] !_checkBit(processedLogs, uint8(logKey) this check is redundant when processedLogs is 0

The bitwise AND operator (&) returns a 1 only if both operands have a 1 in the same position. In this case, processedLogs is 0, which means that all of its bits are 0. Therefore, the result of the AND operation will always be 0, regardless of the value of logKey . If its first iteration in for loop then processedLogs comes with default value 0 .

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

130: require(!_checkBit(processedLogs, uint8(logKey)), "kp");

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L130

[G-9] The claimFailedDeposit() function can be refactored for greater gas efficiency

Gas savings can be achieved by initially verifying the amount before invoking the zkSync.proveL1ToL2TransactionStatus() function. This optimization is due to the costly nature of the zkSync.proveL1ToL2TransactionStatus() function, which should only be called when the amount is greater than zero

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L255-L275

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol

 function claimFailedDeposit(
        address _depositSender,
        address _l1Token,
        bytes32 _l2TxHash,
        uint256 _l2BatchNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBatch,
        bytes32[] calldata _merkleProof
    ) external nonReentrant senderCanCallFunction(allowList) {

+        uint256 amount = depositAmount[_depositSender][_l1Token][_l2TxHash];
+        require(amount > 0, "y1");

        bool proofValid = zkSync.proveL1ToL2TransactionStatus(
            _l2TxHash,
            _l2BatchNumber,
            _l2MessageIndex,
            _l2TxNumberInBatch,
           _merkleProof,
            TxStatus.Failure
       );
        require(proofValid, "yn");

-        uint256 amount = depositAmount[_depositSender][_l1Token][_l2TxHash];
-        require(amount > 0, "y1");

[G-10] Optimize the fallback() function for better gas efficiency

Caching diamondStorage before the msg.data.length >= 4 || msg.data.length == 0 check can result in gas savings unless there's a revert in the require check, saving 2100 GAS

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/DiamondProxy.sol#L20-L25

FILE : 2023-10-zksync/code/contracts/ethereum/contracts/zksync/DiamondProxy.sol

  fallback() external payable {
+        require(msg.data.length >= 4 || msg.data.length == 0, "Ut");
        Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage();
        // Check whether the data contains a "full" selector or it is empty.
        // Required because Diamond proxy finds a facet by function signature,
        // which is not defined for data length in range [1, 3].
-        require(msg.data.length >= 4 || msg.data.length == 0, "Ut");

[G-11] Optimize the commitBatches() function for better gas efficiency

The parameter _newBatchesData.length > 0 should be checked before evaluating s.storedBatchHashes[s.totalBatchesCommitted] == _hashStoredBatchInfo(_lastCommittedBatchData) when dealing with state variables in a function call. This optimization can lead to a savings of 500 GAS

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L183-L185

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

+ 185:        require(_newBatchesData.length > 0, "No batches to commit");
183: // Check that we commit batches after last committed batch
184:        require(s.storedBatchHashes[s.totalBatchesCommitted] == _hashStoredBatchInfo(_lastCommittedBatchData), "i"); // incorrect previous batch data
- 185:        require(_newBatchesData.length > 0, "No batches to commit");

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L183-L185

[G-12] Optimize the requestL2Transaction() function for better gas efficiency

Prioritize the check of the _l2GasPerPubdataByteLimit parameter before performing any other operations, resulting in a savings of 200 GAS

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L257

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol

 function requestL2Transaction(
        address _contractL2,
        uint256 _l2Value,
        bytes calldata _calldata,
        uint256 _l2GasLimit,
        uint256 _l2GasPerPubdataByteLimit,
        bytes[] calldata _factoryDeps,
        address _refundRecipient
    ) external payable nonReentrant senderCanCallFunction(s.allowList) returns (bytes32 canonicalTxHash) {
        // Change the sender address if it is a smart contract to prevent address collision between L1 and L2.
+        require(_l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, "qp");
        // Please note, currently zkSync address derivation is different from Ethereum one, but it may be changed in the future.
        address sender = msg.sender;
        if (sender != tx.origin) {
            sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
        }

        // Enforcing that `_l2GasPerPubdataByteLimit` equals to a certain constant number. This is needed
        // to ensure that users do not get used to using "exotic" numbers for _l2GasPerPubdataByteLimit, e.g. 1-2, etc.
        // VERY IMPORTANT: nobody should rely on this constant to be fixed and every contract should give their users the ability to provide the
        // ability to provide `_l2GasPerPubdataByteLimit` for each independent transaction.
        // CHANGING THIS CONSTANT SHOULD BE A CLIENT-SIDE CHANGE.
-        require(_l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, "qp");

[G-13] Optimize code to avoid to spend unnecessary GAS

[G-13.1] Any revert inside the for loop then caching initAddress ,initCalldata is waste of the computation and gas . Saves 300 GAS. Values of initAddress and initCalldata is used after the for loop. So caching values before for loop is unnecessary.

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol#L97-L98

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol

function diamondCut(DiamondCutData memory _diamondCut) internal {
        FacetCut[] memory facetCuts = _diamondCut.facetCuts;
-        address initAddress = _diamondCut.initAddress;
-        bytes memory initCalldata = _diamondCut.initCalldata;
        uint256 facetCutsLength = facetCuts.length;
        for (uint256 i = 0; i < facetCutsLength; i = i.uncheckedInc()) {
            Action action = facetCuts[i].action;
            address facet = facetCuts[i].facet;
            bool isFacetFreezable = facetCuts[i].isFreezable;
            bytes4[] memory selectors = facetCuts[i].selectors;

            require(selectors.length > 0, "B"); // no functions for diamond cut

            if (action == Action.Add) {
                _addFunctions(facet, selectors, isFacetFreezable);
            } else if (action == Action.Replace) {
                _replaceFunctions(facet, selectors, isFacetFreezable);
            } else if (action == Action.Remove) {
                _removeFunctions(facet, selectors);
            } else {
                revert("C"); // undefined diamond cut action
            }
        }
+        address initAddress = _diamondCut.initAddress;
+        bytes memory initCalldata = _diamondCut.initCalldata;

        _initializeDiamondCut(initAddress, initCalldata);
        emit DiamondCut(facetCuts, initAddress, initCalldata);
    }

[G-13.2] Any revert in block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER <= batchTimestamp check then caching lastL2BlockTimestamp value unnecessary and waste of gas

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L87

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

  function _verifyBatchTimestamp(
        uint256 _packedBatchAndL2BlockTimestamp,
        uint256 _expectedBatchTimestamp,
        uint256 _previousBatchTimestamp
    ) internal view {
        // Check that the timestamp that came from the system context is expected
        uint256 batchTimestamp = _packedBatchAndL2BlockTimestamp >> 128;
        require(batchTimestamp == _expectedBatchTimestamp, "tb");

        // While the fact that _previousBatchTimestamp < batchTimestamp is already checked on L2,
        // we double check it here for clarity
        require(_previousBatchTimestamp < batchTimestamp, "h3");

-        uint256 lastL2BlockTimestamp = _packedBatchAndL2BlockTimestamp & PACKED_L2_BLOCK_TIMESTAMP_MASK;

        // All L2 blocks have timestamps within the range of [batchTimestamp, lastL2BatchTimestamp].
        // So here we need to only double check that:
        // - The timestamp of the batch is not too small.
        // - The timestamp of the last L2 block is not too big.
        require(block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER <= batchTimestamp, "h1"); // New batch timestamp is too small
+        uint256 lastL2BlockTimestamp = _packedBatchAndL2BlockTimestamp & PACKED_L2_BLOCK_TIMESTAMP_MASK;
        require(lastL2BlockTimestamp <= block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA, "h2"); // The last L2 block timestamp is too big
    }



123:  for (uint256 i = 0; i < emittedL2Logs.length; i = i.uncheckedAdd(L2_TO_L1_LOG_SERIALIZE_SIZE)) {
            // Extract the values to be compared to/used such as the log sender, key, and value
-            (address logSender, ) = UnsafeBytes.readAddress(emittedL2Logs, i + L2_LOG_ADDRESS_OFFSET);
            (uint256 logKey, ) = UnsafeBytes.readUint256(emittedL2Logs, i + L2_LOG_KEY_OFFSET);
-            (bytes32 logValue, ) = UnsafeBytes.readBytes32(emittedL2Logs, i + L2_LOG_VALUE_OFFSET);

            // Ensure that the log hasn't been processed already
            require(!_checkBit(processedLogs, uint8(logKey)), "kp");
            processedLogs = _setBit(processedLogs, uint8(logKey));
+            (address logSender, ) = UnsafeBytes.readAddress(emittedL2Logs, i + L2_LOG_ADDRESS_OFFSET);
+            (bytes32 logValue, ) = UnsafeBytes.readBytes32(emittedL2Logs, i + L2_LOG_VALUE_OFFSET);

[G-14] Not initializing variables to their default values incurs extra gas, So leaving them with their default values is more gas-efficient

This is because the default value of 0 is already assigned to value automatically. By not initializing it, we avoid the cost of the unnecessary assignment.

FILE: Breadcrumbs2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

123: for (uint256 i = 0; i < emittedL2Logs.length; i = i.uncheckedAdd(L2_TO_L1_LOG_SERIALIZE_SIZE)) {

209: for (uint256 i = 0; i < _newBatchesData.length; i = i.uncheckedInc()) {

241:  for (uint256 i = 0; i < _newBatchesData.length; i = i.uncheckedInc()) {

263: for (uint256 i = 0; i < _nPriorityOps; i = i.uncheckedInc()) {

293: for (uint256 i = 0; i < nBatches; i = i.uncheckedInc()) {

330: for (uint256 i = 0; i < committedBatchesLength; i = i.uncheckedInc()) {

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L123

[G-15] Don't cache immutable variable incurs extra Gas

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol

function _propagateToZkSync() internal {
-        address contractAddress = zkSyncContract;
       assembly {
           // Copy function signature and arguments from calldata at zero position into memory at pointer position
           calldatacopy(0, 0, calldatasize())
           // Call method of the zkSync contract returns 0 on error
-            let result := call(gas(), contractAddress, 0, 0, calldatasize(), 0, 0)
+            let result := call(gas(), zkSyncContract, 0, 0, calldatasize(), 0, 0)

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L133

[G-16] Bytes constants are more efficient than string constants

bytes constants are more gas efficient than string constants when the string length is fixed. This is because the Ethereum Virtual Machine (EVM) stores bytes constants in a more compact way than string constants.

As per remix gas tests saves 31630 GAS per instance

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

- 22: string public constant override getName = "ExecutorFacet";
+ 22: bytes32 public constant override getName = "ExecutorFacet";

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L22

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol

- 41:  string public constant override getName = "MailboxFacet";
+ 41:  bytes32 public constant override getName = "MailboxFacet";

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L41

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Getters.sol

- 19: string public constant override getName = "GettersFacet";
+ 19: bytes32 public constant override getName = "GettersFacet";

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Getters.sol#L19

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol

- 24:  string public constant override getName = "ValidatorTimelock";
+ 24:  bytes32 public constant override getName = "ValidatorTimelock";

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L24

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Admin.sol

- 15: string public constant override getName = "AdminFacet";
+ 15: bytes32 public constant override getName = "AdminFacet";

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L15

[G-17] Don't cache variable only used once

If the variable is only accessed once, it's cheaper to use the state variable directly that one time, and save the 3 gas the extra stack assignment would spend.

[G-17.1] hasSpecialAccessToCall[_caller][_target][_functionSig] can be used directly instead of local cache

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/common/AllowList.sol#L117

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/common/AllowList.sol

- 117: bool currentPermission = hasSpecialAccessToCall[_caller][_target][_functionSig];

-        if (currentPermission != _enable) {
+        if (hasSpecialAccessToCall[_caller][_target][_functionSig] != _enable) {
            hasSpecialAccessToCall[_caller][_target][_functionSig] = _enable;
            emit UpdateCallPermission(_caller, _target, _functionSig, _enable);
        }

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/common/AllowList.sol#L117

[G-17.1] Using a local variable to cache the result of s.priorityQueue.popFront() is redundant and costs 250 GAS per iteration. Instead, use the result of the function call directly.

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L263-L266

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

263: for (uint256 i = 0; i < _nPriorityOps; i = i.uncheckedInc()) {
- 264:            PriorityOperation memory priorityOp = s.priorityQueue.popFront();
- 265:            concatHash = keccak256(abi.encode(concatHash, priorityOp.canonicalTxHash));
+ 265:            concatHash = keccak256(abi.encode(concatHash, s.priorityQueue.popFront().canonicalTxHash));
266:        }
FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/facets/Executor.sol

-  uint256 lastL2BlockTimestamp = _packedBatchAndL2BlockTimestamp & PACKED_L2_BLOCK_TIMESTAMP_MASK;

        // All L2 blocks have timestamps within the range of [batchTimestamp, lastL2BatchTimestamp].
        // So here we need to only double check that:
        // - The timestamp of the batch is not too small.
        // - The timestamp of the last L2 block is not too big.
        require(block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER <= batchTimestamp, "h1"); // New batch timestamp is too small
-        require(lastL2BlockTimestamp <= block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA, "h2"); // The last L2 block timestamp is too big
+        require(_packedBatchAndL2BlockTimestamp & PACKED_L2_BLOCK_TIMESTAMP_MASK <= block.timestamp + COMMIT_TIMESTAMP_APPROXIMATION_DELTA, "h2"); // The last L2 block timestamp is too big

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L87-L94

[G-18] First perform less expensive checks and then proceed to more costly ones

FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/DiamondProxy.sol

- 31: require(!diamondStorage.isFrozen || !facet.isFreezable, "q1"); // Facet is frozen
+ 31: require(!facet.isFreezable || !diamondStorage.isFrozen, "q1"); // Facet is frozen

https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/DiamondProxy.sol#L31

#0 - 141345

2023-10-26T04:22:25Z

1 L 4 r 7 nc

[G-1] Improving storage efficiency through Struct Packing r

[G-2] Replacing booleans with uint256(1)/uint256(2) for gas optimization L

[G-3] Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate i

[G-4] totalDepositedAmountPerUser[_l1Token][_depositor] should be cached r

[G-5] Efficient event consolidation for Glogtopic Gas Savings r

[G-6] Optimize gas usage by avoiding variable declarations inside loops nc

[G-7] Use dot notation method for struct assignment r

[G-8] !_checkBit(processedLogs, uint8(logKey) this check is redundant when processedLogs is 0 nc

[G-9] The claimFailedDeposit() function can be refactored for greater gas efficiency nc

[G-10] Optimize the fallback() function for better gas efficiency d

[G-11] Optimize the commitBatches() function for better gas efficiency d

[G-12] Optimize the requestL2Transaction() function for better gas efficiency d

[G-13] Optimize code to avoid to spend unnecessary GAS r

[G-14] Not initializing variables to their default values incurs extra gas, So leaving them with their default values is more gas-efficient x

[G-15] Don't cache immutable variable incurs extra Gas nc

[G-16] Bytes constants are more efficient than string constants nc

[G-17] Don't cache variable only used once nc

[G-18] First perform less expensive checks and then proceed to more costly ones nc

#1 - c4-pre-sort

2023-11-02T16:11:27Z

141345 marked the issue as sufficient quality report

#2 - c4-judge

2023-11-15T13:15:18Z

GalloDaSballo marked the issue as grade-b

#3 - sathishpic22

2023-11-30T07:21:38Z

Hi @GalloDaSballo

Struct packing traditionally marked as LOW for Previous contests . I also confirmed this issue with sponsor he accepted this. The protocol versions can be uint96 . This findings alone saves 4000 GAS . Even no other wardens reported this issue

[G-1] Improving storage efficiency through Struct Packing

[G-1.1] protocolVersion and l2SystemContractsUpgradeBatchNumber can be uint96 instead of uint256 : Saves 4000 GAS , 2 SLOT [G-1.2] newProtocolVersion can be uint96 instead of uint256 : Saves 2000 GAS , 1 SLOT

Also saving mapping cache traditionally comes under LOW category

totalDepositedAmountPerUser[_l1Token][_depositor] should be cached : Saves 100 GAS , 1 SLOD

The claimFailedDeposit() function can be refactored for greater gas efficiency

This saves 1 external call if any failures in amount > 0 check

Regarding issue https://github.com/code-423n4/2023-10-zksync-findings/issues/996 there is OOS findings are marked as LOW . These instances are already found in bot reports. 4 Findings marked as LOW . Please check this .

[G-8] totalBatchesCommitted is read twice from storage in Executor::commitBatches() [G-9] totalBatchesVerified is read twice from storage in Executor::proveBatches() [G-10] totalBatchesCommitted, totalBatchesVerified and totalBatchesExecuted are re-read from storage in Executor::revertBatches() [G-13] User deposited amount in Mailbox::_verifyDepositLimit() is read twice from storage

Thank you

#4 - lsaudit

2023-12-01T22:25:33Z

[G-18]

This is invalid, in both cases we're reading from struct. Moreover, it's more likely that faces is freezable, than diamond is frozen. The order is correct, because the more likely scenario is being checked first. If first condition is true, the second one won't be evaluated (because of OR operation). Thus the current implementation saves more gas.

[G-15] and [G-17] are dups,

Both issues are referring to caching local variable and should be counted once

[G-16]

This is invalid getName is override, because it's related to facet's name. Diamond needs to ensure that some facets can be named with more than 32 characters. This cannot be bytes32, it has to be string. This issue is invalid!

[G-13]

This is not refactoring issue, it's just changing the order of require statements (moving require statement up), thus it should be considered as NC. Moreover, since this is just moving require statement up, this should be considered as duplicate, the same type/class of findings is here: G-12, G-11, G-10, G-9

[G-08]

This seems to be invalid, this is required to make sure that log hasn't been processed multiple of times

[G-07]

This is not refactoring issue, it should be NC

[G-05]

Events NewPendingGovernor(), NewGovernor(), NewPendingAdmin(), NewAdmin() guarantee the offchain security of the protocol. They let the users to monitor critical operations (such as admin is being changed, governor is being changed), so user will be able to quickly react in case of emergency.

There are two emergency cases - someone is trying to change the governor (then NewPendingGovernor() is being emitted), the governor is being changed (NewGovernor() is being emitted). Users who monitors for these events can properly react on some critical changes. Adding another event - PendingAndNewGovernor() provides some unnecessary mess, which impacts the overall security of the user.

In case of contract's critical changes, each event should be linked to the action. Current implementation provides that. NewPendingGovernor() is emitted whenever pendingGovernor is being changed. NewGovernor() is being emitted whenever s.governor is being updated. Since acceptGovernor() performs two of these actions (updating s.governor and pendingGoernor), both NewGovernor() and NewPendingGovernor() should be emitted.

[G-1.2] "The newprotocolVersion value is increased in a sequential way" - this is not true. Function _setNewProtocolVersion() can set it to anything.

#5 - sathishpic22

2023-12-02T05:47:03Z

Hi @lsaudit

Thank you for your suggestions.

I think all your understanding and assumptions are wrong.

[G-18] regarding this

require(!diamondStorage.isFrozen || !facet.isFreezable, "q1");

!diamondStorage.isFrozen is a state variable, while !facet.isFreezable is a memory variable. According to best practices for gas optimization, it's more efficient to first check the memory variable !facet.isFreezable. If this check returns true, then it's not necessary to access the state variable !diamondStorage.isFrozen, as memory operations are less costly in terms of gas than state variable accesses. This approach helps in reducing gas consumption and optimizing smart contract performance

[G-15] Its about immutable cache [G-17] its about state variables cache. This is the standard way followed for all findings.

[G-16] I reported about constants not normal variables. Constant values in a Solidity contract are fixed and do not change anywhere in the contract once they are assigned. These values always remain the same, as all instances are constants. Once a constant value is assigned, it cannot be changed

[G-9] regarding this issue if any failures this will save 1 external calls. Yes ,refactoring because the structure of the code is aligned . Any changes in existing code always comes refactoring category

[G-13],[G-12], [G-11], [G-10], [G-9] yes still the code structure is changed. All this suggestions saves gas in particular scenarios.

[G-8] !_checkBit(processedLogs, uint8(logKey) this check is redundant when processedLogs is 0.

Suggestion indicates that the check becomes redundant during the first iteration, as the processedLogs value is initially 0. In this scenario, the condition will always be true for the first iteration, rendering the check unnecessary at that stage

[G-05] Suggestion about combining events in Solidity smart contracts is indeed a recognized gas optimization technique. In scenarios where functions like acceptGovernor() and acceptAdmin() are called, and both trigger separate events, combining these two events into a single event can result in significant gas savings.

By emitting a single event instead of two separate ones, you save on the gas costs associated with each event. Specifically, you conserve 375 gas for each Glogtopic (the cost for a topic in an event log) and an additional 700 gas for the two instances combined. This optimization is beneficial because it reduces the amount of data stored on the blockchain and the computational work needed to process and log these events.

This approach has been accepted in many previous contests, demonstrating its effectiveness in optimizing smart contracts for better gas efficiency

[G-1.2] "The newprotocolVersion value is increased in a sequential way" - this is not true. Function _setNewProtocolVersion() can set it to anything.

This struct optimization alone saves 6000 GAS , 3 SLOTS

Regarding your suggestion, the sponsor has confirmed that the increment within the contract is always sequential, as per their response.

GAXNAHR_@9C68C RKS8$4
8B7N0C~%BBRKUH$CPBY$FO0

I don't know disclosing sponsor name is right or wrong. so i just hide his name

#6 - sathishpic22

2023-12-03T08:39:32Z

Hi @GalloDaSballo

I want to add some additional points here

[G-10] Optimize the fallback() function for better gas efficiency

I don't understand why this is marked as disputed. Any failures in require(msg.data.length >= 4 || msg.data.length == 0, "Ut") mean that creating the storage variable Diamond.DiamondStorage storage diamondStorage = Diamond.getDiamondStorage() and making an external call is a waste of gas. This finding has already been proven and accepted in many old contests.

[G-11] Optimize the commitBatches() function for better gas efficiency

According to Solidity coding standards and for better gas efficiency, parameters should be checked first, followed by function calls and state variable checks

[G-12] Optimize the requestL2Transaction() function for better gas efficiency

If there are any failures in this check _l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, then checking and assigning sender values becomes a waste of gas.

I also want to mention that all my findings include the mitigation steps and gas saved. No other researchers wrote about the mitigation steps and gas saved

I also sent 'many unique findings' and high value findings more than any other wardens.

Struct packing is a prime example of this. This technique alone saves 6000 gas and uses only 3 slots, which no other wardens have reported. This mitigation accepted by sponsor.

"My report is currently the second best. It has 1L, 4R,7NC findings. Usually, the grades are decided based on the L count. Two of my Low findings have been marked as R.

Issue 996 4 of his LOW findings are out of scope that are marked as LOW. If those are marked invalid then my report is best compare to others, Many wardens just spamming with more counts than quality findings. This approach will degrade the wardens who are trying genuine high value findings and wrote with good quality reports.

If the grades based on score then there is no point for good quality findings. many wardens just simply sending bunch of low value NC findings and can get good scores.

Example issue 1070 and 338 sent so many NC critical findings and ignoring and got more score than some of the good reports.

Thank you for this chance to express my thoughts.

#7 - GalloDaSballo

2023-12-10T19:04:02Z

(https://gist.github.com/g-1-improving-storage-efficiency-through-struct-packing) I (https://gist.github.com/g-11-protocolversion-and-l2systemcontractsupgradebatchnumber-can-be-uint96-instead-of-uint256--saves-4000-gas--2-slot) L (https://gist.github.com/g-12--newprotocolversion-can-be-uint96-instead-of-uint256--saves-2000-gas--1-slot) L (https://gist.github.com/g-13--txtype-and-nonce-can-be-uint128-instead-of-uint256--saves-2000-gas--1-slot) I (https://gist.github.com/g-2-replacing-booleans-with-uint2561uint2562-for-gas-optimization) I (https://gist.github.com/g-3-multiple-addressid-mappings-can-be-combined-into-a-single-mapping-of-an-addressid-to-a-struct-where-appropriate) I (https://gist.github.com/g-4-totaldepositedamountperuser_l1token_depositor-should-be-cached) I (https://gist.github.com/g-5-efficient-event-consolidation-for-glogtopic-gas-savings) I (https://gist.github.com/g-6-optimize-gas-usage-by-avoiding-variable-declarations-inside-loops) I (https://gist.github.com/g-7-use-dot-notation-method-for-struct-assignment) I (https://gist.github.com/g-8-_checkbitprocessedlogs-uint8logkey-this-check-is-redundant-when-processedlogs-is-0) NC (https://gist.github.com/g-9-the-claimfaileddeposit-function-can-be-refactored-for-greater-gas-efficiency) I (https://gist.github.com/g-10-optimize-the-fallback-function-for-better-gas-efficiency) I (https://gist.github.com/g-11-optimize-the-commitbatches-function-for-better-gas-efficiency) I (https://gist.github.com/g-12--optimize-the-requestl2transaction-function-for-better-gas-efficiency) I (https://gist.github.com/g-13-optimize-code-to-avoid-to-spend-unnecessary-gas) I (https://gist.github.com/g-14-not-initializing-variables-to-their-default-values-incurs-extra-gas-so-leaving-them-with-their-default-values-is-more-gas-efficient) I (https://gist.github.com/g-15-dont-cache-immutable-variable-incurs-extra-gas) I (https://gist.github.com/g-16-bytes-constants-are-more-efficient-than-string-constants) I (https://gist.github.com/g-17-dont-cache-variable-only-used-once) I () I

I - 18 L - 2 NC - 1

#8 - sathishpic22

2023-12-11T09:46:43Z

Hi @GalloDaSballo

There is an inconsistency in the severity assessment. Different opinions exist between the judge and the presorter. One report marks the same findings as ignored, while another report marks the same findings as NC and L.

Based on this, the grades assigned are affected. It seems that my reports have mistakenly marked some of my findings as 'Ignored', I believe.

From issue https://github.com/code-423n4/2023-10-zksync-findings/issues/624 these findings are accepted in other reports as valid but in my reports those are marked as ignoring findings.

image
image
image
image

My findings

image
image
image
image

From Issue https://github.com/code-423n4/2023-10-zksync-findings/issues/924

image
image
image
image

My findings

image
image
image
image

I request to reassess my findings.

Regards,

#9 - GalloDaSballo

2023-12-15T12:48:32Z

Have reviewed and believe B is acceptable, ultimately I believe the report to be sufficient, but not A, would recommend less findings but higher impact

Findings Information

๐ŸŒŸ Selected for report: zkLotus

Also found by: K42, Sathish9098, catellatech, peanuts, pfapostol

Labels

analysis-advanced
grade-b
sufficient quality report
A-05

Awards

776.8033 USDC - $776.80

External Links

Analysis - zkSync Era

Summary

ListHeadDetails
1Overview of zkSync EraDescription overview of zkSync Era Protocol
2Code review approach and methodologyUnderstand the purpose of the code
3Analysis of the code baseTo evaluate the quality, design, and implementation of the code
4Mechanism ReviewThorough examination, assessment, or evaluation of a mechanism
5Architecture Overview of Zksync EraProject's architecture and features
6Systemic & Centralization RisksEnsure that the system remains robust against systemic and centralization risks
7Competition analysisOther Competitors for Zksyc
8Other RecommendationsGeneral Recommendations
9New insights and learning from this auditLearnings from this audit
10Time spentThe total time spend for this analysis

Overview of zkSync Era

zkSync Era is a robust Layer-2 scaling solution designed to enhance the scalability and efficiency of Ethereum. It achieves this by amalgamating smart contracts deployed on the Ethereum mainnet with zkEVM, allowing for Ethereum Virtual Machine (EVM)-compatible smart contract execution.

This technical overview delves into the core components and functionalities of zkSync Era

zkSync Smart Contracts

The zkSync smart contracts deployed on Ethereum's mainnet form the foundational layer of the zkSync Era protocol. These contracts facilitate various crucial operations and interactions, such as deposits, withdrawals, and executing smart contracts in a Layer-2 environment. They maintain the security and integrity of the Layer-2 network while ensuring compatibility with Ethereum's mainnet.

zkEVM

zkEVM is the technological heart of zkSync Era, providing EVM-compatible smart contract execution within the Layer-2 environment. It leverages Zero-Knowledge Proofs to securely validate transactions and contract interactions, offering high throughput and substantially lower gas fees compared to Ethereum's mainnet. Developers can deploy and run smart contracts on zkEVM with minimal changes to their code.

zk-Circuits

zkSync Era employs zk-Circuits to handle transaction privacy, validate proofs, and maintain the consistency and correctness of the Layer-2 state. These circuits ensure that all operations within zkSync are transparently executed and meet security standards. zk-Circuits play a pivotal role in providing zkSync with its scalability, privacy, and security features.

Code review approach and methodology

In the initial phase of our code review, conducted a thorough assessment of the project's scope. This assessment provided us with a clear understanding of the project's objectives, architecture, and the specific components that needed to be reviewed.

Important Scope Contracts

L1 Contracts
  • Executor.sol ExecutorFacet contract is the core contract in zkSync, responsible for processing and validating batches of transactions. It ensures correct order and timing of batch execution, handles priority operations, and validates Zero-Knowledge Proofs.

  • Mailbox.sol MailboxFacet is a contract used in the zkSync Layer-2 scaling solution for Ethereum. It acts as a communication channel between the Layer-1 (Ethereum) and Layer-2 (zkSync) chains, facilitating the transfer of assets and information between them.

  • TransactionValidator.sol TransactionValidator contract is a Solidity library for validating Layer-1 to Layer-2 (L1 to L2) transactions. It's designed for use in zkSync, a Layer-2 scaling solution

  • Admin.sol
    The AdminFacet contract is responsible for managing access rights within a system. It allows for the transfer of governor and admin roles.

  • L1ERC20Bridge.sol The L1ERC20Bridge contract is used for bridging ERC20 tokens from Ethereum Layer 1 (L1) to zkSync Layer 2 (L2). You can deposit tokens from L1 to L2 using the deposit function. If a deposit fails, you can claim the tokens back using claimFailedDeposit. Additionally, the contract finalizes withdrawals from L2 to L1

  • L1WethBridge.sol The L1WethBridge is a Solidity smart contract designed to facilitate the seamless transfer of Wrapped Ether (WETH) tokens between Layer 1 (L1) and Layer 2 (L2) Ethereum networks

  • Governance.sol The Governance smart contract, inspired by OpenZeppelin's TimelockController and Matter Labs Diamond Proxy upgrade mechanism, facilitates governance tasks and upgrades within zkSync Era governed contracts. It enables the scheduling, execution, and cancellation of operations with different permissions and delays

  • BaseZkSyncUpgrade.sol The BaseZkSyncUpgrade is an abstract smart contract used as a foundation for implementing upgrades in the zkSync protocol. It defines a set of functions and events for managing protocol upgrades, such as changing the protocol version, updating bytecode hashes for L2 components, and transitioning to a new verifier contract. It also provides a structure for representing upgrade proposals

  • AllowList.sol
    The AllowList contract that stores the permissions to call the function on different contracts.The contract is fully controlled by the owner, that can grant and revoke any permissions at any time.

L2 Contracts
  • L1Messenger.sol L1Messenger contract for sending arbitrary length messages to L1 by default ZkSync can send fixed length messages on L1. A fixed length message has 4 parameters senderAddress, isService, key, value

  • ContractDeployer.sol ContractDeployer is system smart contract that is responsible for deploying other smart contracts on zkSync

  • Compressor.sol Compressor Contract with code pertaining to compression for zkEVM. At the moment this is used for bytecode compression and state diff compression validation

  • DefaultAccount.sol The default implementation of account. The bytecode of the contract is set by default for all addresses for which no other bytecodes are deployed.

  • NonceHolder.sol NonceHolder contract used for managing nonces for accounts. Together with bootloader,this contract ensures that the pair (sender, nonce) is always unique, ensuring unique transaction hashes.

  • L2EthToken.sol This contract is used by the bootloader and MsgValueSimulator`/`ContractDeployer system contracts to perform the balance changes while simulating the msg.value Ethereum behavior.

  • MsgValueSimulator.sol The contract responsible for simulating transactions with msg.value inside zkEVM

  • L2ERC20Bridge.sol The default bridge implementation for the ERC20 tokens

  • L2StandardERC20.sol The ERC20 token implementation, that is used in the "default" ERC20 bridge

  • L2WethBridge.sol This contract works in conjunction with the L1WethBridge to streamline the process of bridging WETH tokens between L1 and L2

  • L2Weth.sol The canonical implementation of the WETH token

  • SystemContractsCaller.sol SystemContractsCaller contractcontains the functions to make system calls

    YUL Files
  • EventWriter.yul The EventWriter is a contract or object within a smart contract system that handles the decoding and writing of events using low-level instructions in Ethereum

  • EcAdd.yul The EcAdd object within this smart contract appears to be responsible for implementing the addition operation for elliptic curve points on the alt_bn128 curve

  • EcMul.yul The provided code appears to be a Solidity smart contract named EcMul_deployed implementing elliptic curve point multiplication using the Montgomery form for a specific elliptic curve known as alt_bn128

  • Ecrecover.yul Contract called Ecrecover_deployed, which emulates the behavior of the Ethereum Virtual Machine's (EVM) ecrecover precompile. It uses the precompileCall function to call zkEVM's built-in precompiles for cryptographic signature recovery

  • Keccak256.yul Contract, named Keccak256_deployed, emulates the behavior of Ethereum's keccak256 opcode

  • SHA256.yul Contract, SHA256_deployed, emulates Ethereum's sha256 precompile functionality

Analysis of the code base

I've presented a detailed technical explanation of the real-time operation of key processes in the ZKSync era, including their flow, illustrated through flow diagrams

Flow of Commit New Batches

function commitBatches(StoredBatchInfo memory _lastCommittedBatchData, CommitBatchInfo[] calldata _newBatchesData)
        external
        override
        nonReentrant
        onlyValidator
    {
        // Check that we commit batches after last committed batch
        require(s.storedBatchHashes[s.totalBatchesCommitted] == _hashStoredBatchInfo(_lastCommittedBatchData), "i"); // incorrect previous batch data
        require(_newBatchesData.length > 0, "No batches to commit");

        bytes32 systemContractsUpgradeTxHash = s.l2SystemContractsUpgradeTxHash;
        // Upgrades are rarely done so we optimize a case with no active system contracts upgrade.
        if (systemContractsUpgradeTxHash == bytes32(0) || s.l2SystemContractsUpgradeBatchNumber != 0) {
            _commitBatchesWithoutSystemContractsUpgrade(_lastCommittedBatchData, _newBatchesData);
        } else {
            _commitBatchesWithSystemContractsUpgrade(
                _lastCommittedBatchData,
                _newBatchesData,
                systemContractsUpgradeTxHash
            );
        }

        s.totalBatchesCommitted = s.totalBatchesCommitted + _newBatchesData.length;
    } 
Commiting New Batches

Verifying the Transmission of an Arbitrary-Length Message within a Designated L2 Batch


 function proveL2MessageInclusion(
        uint256 _batchNumber,
        uint256 _index,
        L2Message memory _message,
        bytes32[] calldata _proof
    ) public view returns (bool) {
        return _proveL2LogInclusion(_batchNumber, _index, _L2MessageToLog(_message), _proof);
    }
Prove that a specific arbitrary-length message was sent in a specific L2 batch number- MailBox

Execution flow of L2 transaction from L1


function requestL2Transaction(
        address _contractL2,
        uint256 _l2Value,
        bytes calldata _calldata,
        uint256 _l2GasLimit,
        uint256 _l2GasPerPubdataByteLimit,
        bytes[] calldata _factoryDeps,
        address _refundRecipient
    ) external payable nonReentrant senderCanCallFunction(s.allowList) returns (bytes32 canonicalTxHash) {
        // Change the sender address if it is a smart contract to prevent address collision between L1 and L2.
        // Please note, currently zkSync address derivation is different from Ethereum one, but it may be changed in the future.
        address sender = msg.sender;
        if (sender != tx.origin) {
            sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
        }

        // Enforcing that `_l2GasPerPubdataByteLimit` equals to a certain constant number. This is needed
        // to ensure that users do not get used to using "exotic" numbers for _l2GasPerPubdataByteLimit, e.g. 1-2, etc.
        // VERY IMPORTANT: nobody should rely on this constant to be fixed and every contract should give their users the ability to provide the
        // ability to provide `_l2GasPerPubdataByteLimit` for each independent transaction.
        // CHANGING THIS CONSTANT SHOULD BE A CLIENT-SIDE CHANGE.
        require(_l2GasPerPubdataByteLimit == REQUIRED_L2_GAS_PRICE_PER_PUBDATA, "qp");

        // The L1 -> L2 transaction may be failed and funds will be sent to the `_refundRecipient`,
        // so we use `msg.value` instead of `_l2Value` as the bridged amount.
        _verifyDepositLimit(msg.sender, msg.value);
        canonicalTxHash = _requestL2Transaction(
            sender,
            _contractL2,
            _l2Value,
            _calldata,
            _l2GasLimit,
            _l2GasPerPubdataByteLimit,
            _factoryDeps,
            false,
            _refundRecipient
        );
    }
execution flow of L2 transaction from L1- MailBox

Validation key properties of an L1->L2 transaction

 function validateL1ToL2Transaction(
        IMailbox.L2CanonicalTransaction memory _transaction,
        bytes memory _encoded,
        uint256 _priorityTxMaxGasLimit
    ) internal pure {
        uint256 l2GasForTxBody = getTransactionBodyGasLimit(
            _transaction.gasLimit,
            _transaction.gasPerPubdataByteLimit,
            _encoded.length
        );

        // Ensuring that the transaction is provable
        require(l2GasForTxBody <= _priorityTxMaxGasLimit, "ui");
        // Ensuring that the transaction cannot output more pubdata than is processable
        require(l2GasForTxBody / _transaction.gasPerPubdataByteLimit <= PRIORITY_TX_MAX_PUBDATA, "uk");

        // Ensuring that the transaction covers the minimal costs for its processing:
        // hashing its content, publishing the factory dependencies, etc.
        require(
            getMinimalPriorityTransactionGasLimit(
                _encoded.length,
                _transaction.factoryDeps.length,
                _transaction.gasPerPubdataByteLimit
            ) <= _transaction.gasLimit,
            "up"
        );
    }
validate key properties of an L1-L2 transaction

FinalizeWithdrawal from L2 to L1

function finalizeWithdrawal(
        uint256 _l2BatchNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBatch,
        bytes calldata _message,
        bytes32[] calldata _merkleProof
    ) external nonReentrant senderCanCallFunction(allowList) {
        require(!isWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex], "pw"); 

        L2Message memory l2ToL1Message = L2Message({
            txNumberInBatch: _l2TxNumberInBatch,
            sender: l2Bridge,
            data: _message
        });

        (address l1Receiver, address l1Token, uint256 amount) = _parseL2WithdrawalMessage(l2ToL1Message.data);
        // Preventing the stack too deep error
        {
            bool success = zkSync.proveL2MessageInclusion(_l2BatchNumber, _l2MessageIndex, l2ToL1Message, _merkleProof);
            require(success, "nq");
        }

        isWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex] = true; 
        // Withdraw funds
        IERC20(l1Token).safeTransfer(l1Receiver, amount);

        emit WithdrawalFinalized(l1Receiver, l1Token, amount);
    }
FinalizeWithdrawalfromL2to L1

Upgrade Process

function upgrade(ProposedUpgrade calldata _proposedUpgrade) public override returns (bytes32) {
        super.upgrade(_proposedUpgrade);

        _setNewProtocolVersion(_proposedUpgrade.newProtocolVersion);
        _upgradeL1Contract(_proposedUpgrade.l1ContractsUpgradeCalldata);
        _upgradeVerifier(_proposedUpgrade.verifier, _proposedUpgrade.verifierParams);
        _setBaseSystemContracts(_proposedUpgrade.bootloaderHash, _proposedUpgrade.defaultAccountHash);

        bytes32 txHash;
        txHash = _setL2SystemContractUpgrade(
            _proposedUpgrade.l2ProtocolUpgradeTx,
            _proposedUpgrade.factoryDeps,
            _proposedUpgrade.newProtocolVersion
        );

        _setAllowList(IAllowList(_proposedUpgrade.newAllowList));
        _postUpgrade(_proposedUpgrade.postUpgradeCalldata);

        emit UpgradeComplete(_proposedUpgrade.newProtocolVersion, txHash, _proposedUpgrade);

        return Diamond.DIAMOND_INIT_SUCCESS_RETURN_VALUE;
    }
Upgrade-Process

Message Sending Process Form L2 to L1


function sendToL1(bytes calldata _message) external override returns (bytes32 hash) {
        uint256 gasBeforeMessageHashing = gasleft();
        hash = EfficientCall.keccak(_message);
        uint256 gasSpentOnMessageHashing = gasBeforeMessageHashing - gasleft();

        /// Store message record
        chainedMessagesHash = keccak256(abi.encode(chainedMessagesHash, hash));

        /// Store log record
        L2ToL1Log memory l2ToL1Log = L2ToL1Log({
            l2ShardId: 0,
            isService: true,
            txNumberInBlock: SYSTEM_CONTEXT_CONTRACT.txNumberInBlock(),
            sender: address(this),
            key: bytes32(uint256(uint160(msg.sender))),
            value: hash
        });
        _processL2ToL1Log(l2ToL1Log);

        // Get cost of one byte pubdata in gas from context.
        uint256 meta = SystemContractHelper.getZkSyncMetaBytes();
        uint32 gasPerPubdataBytes = SystemContractHelper.getGasPerPubdataByteFromMeta(meta);

        uint256 pubdataLen;
        unchecked {
            // 4 bytes used to encode the length of the message (see `publishPubdataAndClearState`)
            // L2_TO_L1_LOG_SERIALIZE_SIZE bytes used to encode L2ToL1Log
            pubdataLen = 4 + _message.length + L2_TO_L1_LOG_SERIALIZE_SIZE;
        }

        // We need to charge cost of hashing, as it will be used in `publishPubdataAndClearState`:
        // - keccakGasCost(L2_TO_L1_LOG_SERIALIZE_SIZE) and keccakGasCost(64) when reconstructing L2ToL1Log
        // - keccakGasCost(64) and gasSpentOnMessageHashing when reconstructing Messages
        // - at most 2 times keccakGasCost(64) (as merkle tree can contain ~2*N leaves)
        uint256 gasToPay = pubdataLen *
            gasPerPubdataBytes +
            keccakGasCost(L2_TO_L1_LOG_SERIALIZE_SIZE) +
            4 *
            keccakGasCost(64) +
            gasSpentOnMessageHashing;
        SystemContractHelper.burnGas(Utils.safeCastToU32(gasToPay));

        emit L1MessageSent(msg.sender, hash, _message);
    }
send messages to L1 FromL2- L1messenger

The Deposit Flow

 function deposit(
        address _l2Receiver,
        address _l1Token,
        uint256 _amount,
        uint256 _l2TxGasLimit,
        uint256 _l2TxGasPerPubdataByte
    ) external payable returns (bytes32 l2TxHash) {
        l2TxHash = deposit(_l2Receiver, _l1Token, _amount, _l2TxGasLimit, _l2TxGasPerPubdataByte, address(0));
    }
L1toL2Depositflow

Mechanism Review

Zksync Distinctive Patterns

Hyperscaling

Hyperchains are parallel zkEVM instances interconnected through Hyperbridges, akin to how the web uses hyperlinks for site navigation. They share a common settlement layer on the Ethereum L1 mainnet

ZK rollups

  • ZK rollups, short for Zero-Knowledge Rollups, are a technology used in the Ethereum ecosystem to enhance the scalability and efficiency of the network.
  • In a ZK rollup, a batch of transactions is verified for correctness off-chain (outside the Ethereum network), utilizing zero- knowledge proofs. Once this batch is confirmed to be valid through the zero-knowledge proof, only a single succinct proof is submitted to the Ethereum network.

Zero-knowledge proof (ZKP)

  • ZKPs are a cryptographic technique that allows one party to prove to another party that they know a certain piece of information without revealing the information itself .

zkSync specific opcodes

These opcodes are allowed only for contracts in kernel space (i.e. system contracts). If executed in other places they result in revert(0,0)

  • mimic_call - Same as a normal call, but it can alter the msg.sender field of the transaction
  • to_l1 - Sends a system L2โ†’L1 log to Ethereum
  • event - Emits an L2 log to zkSync
  • precompile_call - This is an opcode that accepts two parameters: the uint256 representing the packed parameters for it as well as the ergs to burn.
  • If it is the address of the ecrecover system contract, it performs the ecrecover operation
  • If it is the address of the sha256/keccak256 system contracts, it performs the corresponding hashing operation.
  • setValueForNextFarCall - Sets msg.value for the next call/mimic_call
  • increment_tx_counter - Increments the counter of the transactions within the VM

Opcodes that can be generally accessed by any contract

  • near_call - framed jump to some location of the code of your contract
  • getMeta - Returns an u256 packed value of ZkSyncMeta struct
  • getCodeAddress - Receives the address of the executed code

Flags for calls

  • r1- The pointer to the calldata
  • r2 - This is a mask, where each bit is set only if certain flags have been set to the call Two flags are supported
    • 0-th bit: isConstructor flag
    • 1-st bit: isSystem flag
  • r3 - r12 registers are non-empty only if the isSystem flag is set

Existing Patterns Used

Access Control
  • On-chain access controls The zkSync protocol defines a number of on-chain roles that control access to different parts of the system. For example, the OnlyGovernor role and onlyGovernorOrAdmin is responsible for managing the system's parameters.
function setPendingGovernor(address _newPendingGovernor) external onlyGovernor {

function setPendingAdmin(address _newPendingAdmin) external onlyGovernorOrAdmin {
  • Off-chain access controls zkSync uses a permissioned key management system (KMS) to store the private keys that are used to sign transactions.
Token Standards

zkSync supports deposits of ETH and any ERC-20 token . Users can deposit tokens from the Ethereum mainnet to zkSync by using the zkSync bridge

Architecture Overview of Zksync Era

Hyperscaling

<img width="3780" alt="hyperbridges-2fe1f60d" src="https://user-images.githubusercontent.com/58845085/277155352-d30444b2-0c3d-49d4-9260-5c3b61456aba.png">
Why Hyperscaling ?

Hyperchains address the pressing need for scalability, interoperability, and security in the blockchain space. They provide a framework for a more scalable, interconnected, and secure blockchain ecosystem while promoting innovation and flexibility through a permissionless and modular approach.

How Hyperbridges work
<img width="3385" alt="hyperscalingBridgingFull-80a704a4" src="https://user-images.githubusercontent.com/58845085/277155595-d89c5316-e09b-4f0f-a9a7-de111ef90d66.png">
  • Sending Hyperchain settles its proof on L1.
  • This updates the Transaction Root, committing to Hyperbridge transactions.
  • Receiving Hyperchain imports the Transaction Root.
  • A relayer sends the transaction with a Merkle Proof.
  • The proof is verified, and if correct, the transaction is executed.
  • The receiving Hyperchain settles its proof, verifying the imported Transaction Root.

Architecture Risk in Hyperchain architecture

Architecture Complexity

The Hyperchains architecture is complex, and it may be difficult for developers to understand and use.

The lack of a clear governance model

The governance of Hyperchains is unclear. This could lead to disputes between Hyperchains that could be difficult to resolve.

The use of Hyperbridges

Hyperbridges are used to connect Hyperchains together. However, Hyperbridges could be used to create malicious Hyperchains that could harm the Hyperchains ecosystem.

The reliance on a single zkEVM engine

If a vulnerability is found in the zkEVM engine, all Hyperchains could be affected. This is because all Hyperchains are required to use the same zkEVM engine in order to be interoperable.

Blocks in zkSync Era

In zkSync there are two notions of blocks: L2 block and L1 rollup block

L2 blocks - L2 blocks, also known simply as "blocks," are created on the L2 network (zkSync Era) L1 rollup blocks - L1 rollup blocks are batches of consecutive L2 blocks that contain all the transactions in the same order, from the first block to the last block in the batch

Block Properties

Hash: Block's unique identifier (null if pending). ParentHash: Hash of the parent block in L2. Number: Block's sequence number (null if pending). Timestamp: Time when the block was created. Nonce: Latest transaction count for an account (null if pending). Difficulty: Fixed at 2.5 quadrillion (zkSync uses a different consensus). GasLimit: Maximum gas allowed in hexadecimal, always 2^32-1. GasUsed: Actual gas consumed in the block. Transactions: Array of transaction objects. BaseFeePerGas: EIP1559-like baseFee for the block.

Transaction data on zkSync Era

Transactions on zkSync Era are similar to Ethereum transactions so you can use the same wallet as you use on Ethereum. There are some minor differences, mostly with regards to setting the fees.

Whether it originated on Layer 1 (L1): ``is_l1_originated`` (boolean). Transaction status: ``status`` (Pending, Included, Verified, or Failed). Transaction fee: ``fee`` (U256, related to the fee mechanism). Initiator's address: ``initiator_address``. Receipt timestamp: ``received_at`` (in Utc format). Ethereum commitment transaction hash: ``eth_commit_tx_hash`` (optional). Ethereum proof transaction hash: ``eth_prove_tx_hash`` (optional). Ethereum execution transaction hash: ``eth_execute_tx_hash`` (optional).

Fee mechanism

zkSync Era's fee model is similar to Ethereumโ€™s where gas is charged for computational cost, cost of publishing data on-chain and storage effects. However, zkSync Era includes additional costs for publishing to L1 and for proof generation.

gasPrice: the price, in gwei, of a unit of gas. gasPerPubdata: the amount of gas for publishing one byte of data on Ethereum.

Risks of Unpredictable transaction fees

The L1 gas price is volatile, which means that the amount of gas required to publish data to L1 can vary. This can make it difficult for users to predict the cost of transactions.

Risk of Reduced throughput

The proof generation process can be computationally expensive. This can limit the throughput of the zkSync Era network.

Bridges

Bridge is implemented by having two contracts L1ERC20Bridge , L2ERC20Bridge (one deployed to L1, and the second deployed to L2) communicating with each other using L1 <-> L2 interoperability.

System contracts

  • ContractDeployer
  • L1Messenger
  • NonceHolder
  • Bootloader

L1 Smart contracts

  • Diamond
  • DiamondProxy
  • DiamondInit
  • DiamondCutFacet
  • GettersFacet
  • GovernanceFacet
  • MailboxFacet
  • ExecutorFacet

Smart contract development

zkSync Era allows developers to build projects using the same programming languages and tools used to build on Ethereum

Languages Support
  • Solidity support
  • Vyper support

Compilers

  • zksolc
  • zkvyper
Benefits of zkSync Era
  • High scalability: zkSync Era can process thousands of transactions per second, making it a highly scalable solution for Ethereum.
  • Low fees: zkSync Era transactions are much cheaper than Ethereum mainchain transactions.
  • Fast settlement: zkSync Era transactions are settled within minutes, compared to hours or days on the Ethereum mainchain.
  • EVM compatibility: zkSync Era is compatible with the Ethereum Virtual Machine (EVM), which means that existing Ethereum applications can be deployed on zkSync without any modifications.

Systemic & Centralization Risks

Genaral Systemic Risks

Reliance on the sequencer

The sequencer is a critical component of zkSync Era. The sequencer is responsible for ordering transactions and generating blocks. If the sequencer is compromised, it could result in a number of problems, such as the loss of funds or the double-spending of tokens.

Complexity of the zk-proof generation process

The zk-proof generation process is a complex and computationally expensive process. If there is a bug in the zk-proof generation process, it could result in the generation of invalid zk-proofs. This could lead to the rejection of valid transactions or the acceptance of invalid transactions

Centralization of the Hyperchains architecture

The Hyperchains architecture is a centralized architecture. This means that a small number of actors have a large amount of control over the network

Collision attacks

A collision attack is an attack in which an attacker is able to generate two different transactions that have the same hash. This could allow the attacker to double-spend tokens`` or to manipulate the state of the network.

Execution delay

zkSync Era uses a technique called execution delay to ensure the security of the network. Execution delay means that transactions are not executed immediately on L2. Instead, they are first batched together and then executed on L1. This can lead to a delay of several minutes between the time a transaction is submitted and the time it is executed.

Gas cost

The gas cost of using zkSync Era may consume more gas. This is because zkSync Era needs to generate zk-proofs for every transaction. The zk-proof generation process is computationally expensive, and this cost is passed on to users in the form of higher gas fees.

Limited support

zkSync Era is a relatively new technology, and it is not yet supported by all wallets and exchanges. This means that users may have difficulty using zkSync Era if they do not have a wallet or exchange that supports it.

Competition

zkSync Era is not the only scaling solution for Ethereum. There are a number of other scaling solutions being developed, such as Optimistic Rollups and Plasma. These other scaling solutions have their own advantages and disadvantages, and it is not clear which solution will ultimately be the most successful.

Smart contract risk

As with any smart contract, there is always the risk of bugs or vulnerabilities in the zkSync code. If a bug is found, it could result in the loss of funds or the manipulation of the state of the network.

User error

zkSync is a complex system, and there is always the risk of user error. For example, a user could accidentally send funds to the wrong address or lose their private keys. This could result in the loss of funds.

Here's an analysis of potential systemic and centralization risks in the provided contracts

Executor :

Systemic Risk:

  • There's a risk of incorrect contract replacements or failed upgrades
  • Gas costs are not explicitly checked in the code. If the gas consumption exceeds block gas limits, transactions could fail
  • The correct finalization of L2 to L1 logs is essential for data integrity. If logs are not processed correctly, it can lead to incorrect data being committed to the Ethereum mainnet

Cenralization Risks : Centralization risks associated with these functions, primarily due to the onlyValidator modifier. The functions proveBatches(), revertBatches(), commitBatches(), and executeBatches() are only accessible to an entity or a group of validators.

Mailbox

Systemic Risk:

  • The correctness of the proveL2MessageInclusion function is crucial. If it fails to validate the inclusion of L2 logs, it could lead to improper withdrawal or transaction processing, potentially causing financial losses or inconsistencies.
  • The contract enforces a maximum gas limit for priority transactions (s.priorityTxMaxGasLimit). Inadequate estimation of gas limits could lead to transaction failures or reordering issues.

TransactionValidator

Systemic Risk:

  • External dependencies like OpenZeppelin. Systemic risks can emerge if vulnerable versions are used
  • In the validateL1ToL2Transaction function, validate the input data, particularly the _encoded and _transaction parameters. Verify that the _encoded data is not excessively large or potentially malformed, which could lead to out-of-gas or other vulnerabilities
  • Verify the logic in the getOverheadForTransaction function, especially the calculations related to overhead, tx slot overhead, and overhead for gas.

Admin

Cenralization Risks :


 function setPendingGovernor(address _newPendingGovernor) external onlyGovernor {
        // Save previous value into the stack to put it into the event later
        address oldPendingGovernor = s.pendingGovernor;
        // Change pending governor
        s.pendingGovernor = _newPendingGovernor;
        emit NewPendingGovernor(oldPendingGovernor, _newPendingGovernor);
    }

    /// @notice Accepts transfer of governor rights. Only pending governor can accept the role.
    function acceptGovernor() external {
        address pendingGovernor = s.pendingGovernor;
        require(msg.sender == pendingGovernor, "n4"); // Only proposed by current governor address can claim the governor rights

        address previousGovernor = s.governor;
        s.governor = pendingGovernor;
        delete s.pendingGovernor;

        emit NewPendingGovernor(pendingGovernor, address(0));
        emit NewGovernor(previousGovernor, pendingGovernor);
    }
       

The provided code shows a mechanism for transferring governance rights, but it lacks additional safeguards to mitigate certain risks

Unrestricted Governor Power

The current implementation allows the pending governor to unilaterally accept the role of governor. In a decentralized system, it's generally more secure to have some form of community consensus, time delay, or additional checks before a change in governance is accepted.

No Limit on Governance Transfers

The code doesn't appear to limit the number of times governance can be transferred. This could potentially lead to abuse or frequent changes, which might not be in the best interest of the project or the community.

Lack of Time Locks

A time delay mechanism is often used in governance contracts to give the community time to react to changes. In this code, the transfer of governance is immediate, which doesn't allow for a period of consideration or intervention if a malicious or mistaken transfer occurs.

Pending governor could indefinitely delay or ignore the acceptance of governance rights

The absence of a time limit to accept governance changes to the pendingGovernor introduces a potential risk. If there's no time limit, a pending governor could indefinitely delay or ignore the acceptance of governance rights, which could disrupt the governance process and potentially lead to centralization risks

Mitigation Steps

  • Multi-Signature or Governance Module
  • Time Delay
  • Consensus Mechanisms

L1ERC20Bridge

Systemic Risk:

  • The contract uses hardcoded addresses for zkSync and _l2TokenBeacon. It's essential to ensure that these addresses are accurate
  • During contract initialization, it requires specific gas fees for deploying the L2 bridge implementation and L2 bridge proxy. Any discrepancies in these fees can lead to deployment failures.
  • The nonReentrant modifier is used to prevent reentrancy attacks. Ensure that it effectively protects the contract from reentrancy risks in all relevant functions
  • The presence of two deposit methods can be confusing for users
  • Confirm that deposit limits are enforced correctly and cannot be bypassed, especially if the deposit limit information can change

L2ERC20Bridge

Systemic Risk:

  • Verify that value transfers are handled securely. The contract currently requires msg.value to be 0 for ERC20 bridge operations, but ensure that no unexpected value transfers can occur
  • The contract uses proxy deployment to create L2 tokens and token proxies. Ensure that the proxy deployment mechanism is secure and properly configured to prevent unauthorized contract creation
  • Verify that the bridgeMint and bridgeBurn functions in the L2StandardToken are implemented correctly, as they are crucial to deposit and withdrawal functionality.
  • The contract maps L1 token addresses to their L2 counterparts. Ensure that this mapping is accurate and properly maintained to avoid discrepancies between L1 and L2 token addresses
  • The sendMessageToL1 function is used to pass messages to the L1 chain. Ensure that this messaging mechanism is secure and that the messages are properly formatted to prevent unexpected behavior.
  • Verify that functions that interact with other contracts, such as bridgeMint, bridgeBurn, and sendMessageToL1, are protected against reentrancy attacks

Compressor

Systemic Risk:

  • The _decodeRawBytecode function is responsible for extracting the dictionary and encoded data from raw compressed data. Ensure that this decoding process is correct to prevent any data corruption
  • The contract performs various size and length checks, including dictionary length, encoded data length, and compressed value length. Ensure these checks are valid and protect against unexpected data lengths that could lead to out-of-bounds access.
  • The _verifyValueCompression function validates the correctness of value compression given the initial value, final value, and operation. Ensure that this validation process is accurate and prevents erroneous compression

Cenralization Risks : publishCompressedBytecode() is with Administrator control

L1Messenger

Systemic Risk:

  • Risks may arise if these external contracts have vulnerabilities or are upgraded without proper validation
  • The contract performs various verifications on incoming pubdata, including state diffs, Merkle trees, and more. Any errors or discrepancies in the verification process could lead to incorrect data being sent to L1
  • Ensure that the contract doesn't expose itself to reentrancy attacks and carefully validate data from untrusted sources
  • Large or costly operations may hit the block gas limit, causing transactions to revert
  • Potential issues if there are changes or upgrades to the Ethereum network that may affect contract functionality

Cenralization Risks : sendL2ToL1Log() is with Administrator control

Competition analysis

CompetitorDescriptionAdvantagesDisadvantages
OptimismOptimistic rollupFast withdrawals, low feesFraud proofs can be slow
ArbitrumOptimistic rollupFast withdrawals, low feesFraud proofs can be slow
StarkNetzk-rollupHigh throughput, low feesLimited ecosystem
Immutable Xzk-rollupDesigned for NFTs, high throughputLimited ecosystem
Metiszk-rollupEVM-compatible, low feesLimited ecosystem

zkSync Era vs. Optimistic rollups

Optimistic rollups assume that transactions are valid and only challenge them if there is evidence of fraud. This allows for fast withdrawals, as transactions can be finalized immediately. However, if a transaction is challenged, it can take several days for the fraud proof to be processed.

zkSync Era does not assume that transactions are valid. Instead, it generates a zk-proof for every transaction. This means that withdrawals are not as fast as with optimistic rollups, as the zk-proof must be generated before the transaction can be finalized. However, zkSync Era is more secure than optimistic rollups, as there is no risk of fraud.

zkSync Era vs. other zk-rollups

zkSync Era is unique in that it uses a Hyperchains architecture. This allows for different Hyperchains to be customized to meet the needs of different applications. Additionally, Hyperchains can be used to experiment with new features and technologies without affecting the main zkSync Era network.

StarkNet is another promising zk-rollup that is known for its high throughput. StarkNet is able to process a large number of transactions per second, making it a good choice for applications that require high scalability.

Immutable X is a zk-rollup that is designed specifically for NFTs. Immutable X offers a number of features that are beneficial for NFTs, such as gas-free minting and trading.

Metis is a zk-rollup that is EVM-compatible. This means that existing Ethereum applications can be deployed on Metis without any modifications. Metis is a good choice for developers who want to use a zk-rollup without having to learn a new programming language.

Other Recommendations

Test Coverage

The audit scope of the contracts should be the aim of reaching 100% test coverage. However, the tests provided by the protocol are at a high level of comprehension, giving us the opportunity to easily conduct Proof of Concepts (PoCs) to test certain bugs present in the project.

Suggests to add tests attack vectors, especially re-entrancy

The impact of a reentrancy vulnerability can be severe. An attacker can drain funds from the vulnerable contract, manipulate its state, or even render it unusable. Even reentrancy guard added should be tested

Attack vectors, especially re-entrancy, seem untested and trusted

Use More recent version of solidity


pragma solidity ^0.8.0;
pragma solidity ^0.8.13;

It's crucial to use consistent and up-to-date versions to avoid errors, benefit from gas optimizations, and leverage the latest features and improvements. Mixing different versions of Solidity can lead to compatibility issues and unexpected behavior.

Use latest version of OpenZeppelin Contracts

Currently contracts using vulnerable version like V4.8.0. Use atleast V4.9.2 to avoid known bugs

OpenZeppelin Contracts (last updated v4.8.0)

Consider Adding OpenZeppelin access control

OpenZeppelin provides a library for role-based access control, which allows you to manage who can execute specific functions within your smart contract

import "@openzeppelin/contracts/access/AccessControl.sol";

New insights and learning from this audit

From this audit, I have learned about the Hyperchains architecture, zk rollups, bridges, zk opcodes, the fees structure, the batching process, zero knowledge proofs, and L1toL2 meesagings.

Overall, I believe that zkSync Era is a promising scaling solution for Ethereum. The Hyperchains architecture is a novel and flexible approach to scaling, and the zkEVM is a powerful tool that can be used to create secure and scalable smart contracts.

Time spent

50 hours

Time spent:

50 hours

#0 - c4-pre-sort

2023-11-02T15:59:08Z

141345 marked the issue as sufficient quality report

#1 - c4-judge

2023-11-21T16:51:01Z

GalloDaSballo marked the issue as grade-b

#2 - sathishpic22

2023-11-30T08:57:49Z

Hello @GalloDaSballo,

I appreciate your detailed feedback on the evaluation scores for "Attack Mindset (1)," "Tests (.5)," and "SWE (2)."

Attack Mindset (1):

I thoroughly covered potential risks in both the systemic risks sections and architecture recommendation sections. My analysis extends to the codebase, where I explicitly outlined plausible risks. These insights are grounded in documentation and the actual code, ensuring a comprehensive evaluation.

Tests (.5): I addressed the testing aspect in the Test Coverage section of my reports.

While the basis for the SWE (2) score is unclear, I stand by the technical merit of my reports. I've provided a thorough analysis, delving into the codebase intricacies, and presenting my findings in a clear and structured manner. I believe this justifies a higher score in the Software Engineering category.

In addition to the main evaluation criteria, I went beyond expectations by conducting a comprehensive analysis of the competition for ZKSYNC.

I kindly request a reevaluation of my analysis reports, as I believe they meet the criteria for higher grades. Your consideration is greatly appreciated.

Thank you for your time and attention.

#3 - GalloDaSballo

2023-12-02T10:45:39Z

<img width="901" alt="Screenshot 2023-12-02 at 11 45 00" src="https://github.com/code-423n4/2023-10-zksync-findings/assets/13383782/8117d190-4604-45d7-b4b7-b9c5f03c20da"> This is a sample of comments that are self-evident, map out to "lack of address(0)" and overall contribute to a negative judgment from my part as you may have added some SWE advice, but it is hidden under layers of padding
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