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
Rank: 29/64
Findings: 1
Award: $1,495.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: lsaudit
Also found by: 0xAnah, 0xhacksmithh, 0xsomeone, SM3_SS, Sathish9098, albahaca, c3phas, hunter_w3b
1495.6673 USDC - $1,495.67
emit
could used differently to save gasVia doing so * 21gas * can be saved with each emit
function setValidator(address _newValidator) external onlyOwner { - address oldValidator = validator; - validator = _newValidator; - emit NewValidator(oldValidator, _newValidator); + emit NewValidator(validator, validator = _newValidator); }
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L55
event
one step above so that we can save gas cost for one memory
variableHere NewPendingGovernor(oldPendingGovernor, _newPendingGovernor)
could emited one step above so that we need not to create address oldPendingGovernor
stack variable
function setPendingGovernor(address _newPendingGovernor) external onlyGovernor { // Save previous value into the stack to put it into the event later - address oldPendingGovernor = s.pendingGovernor; + emit NewPendingGovernor(s.pendingGovernor, _newPendingGovernor); // Change pending governor s.pendingGovernor = _newPendingGovernor; - emit NewPendingGovernor(oldPendingGovernor, _newPendingGovernor); }
file: contracts/zksync/facets/Admin.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L25
function setPendingAdmin(address _newPendingAdmin) external onlyGovernorOrAdmin { // Save previous value into the stack to put it into the event later - address oldPendingAdmin = s.pendingAdmin; + emit NewPendingGovernor(s.pendingAdmin, _newPendingAdmin); // Change pending admin s.pendingAdmin = _newPendingAdmin; - emit NewPendingGovernor(oldPendingAdmin, _newPendingAdmin); }
file: contracts/zksync/facets/Admin.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L49
- address previousGovernor = s.governor; + emit NewGovernor(s.governor, pendingGovernor); s.governor = pendingGovernor; delete s.pendingGovernor; emit NewPendingGovernor(pendingGovernor, address(0)); - emit NewGovernor(previousGovernor, pendingGovernor);
file: contracts/zksync/facets/Admin.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L38
function acceptAdmin() external { address pendingAdmin = s.pendingAdmin; require(msg.sender == pendingAdmin, "n4"); - address previousAdmin = s.admin; + emit NewAdmin(s.admin, pendingAdmin); s.admin = pendingAdmin; delete s.pendingAdmin; emit NewPendingAdmin(pendingAdmin, address(0)); - emit NewAdmin(previousAdmin, pendingAdmin);
file: contracts/zksync/facets/Admin.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L62
- uint256 oldPriorityTxMaxGasLimit = s.priorityTxMaxGasLimit; + emit NewPriorityTxMaxGasLimit(s.priorityTxMaxGasLimit, _newPriorityTxMaxGasLimit); s.priorityTxMaxGasLimit = _newPriorityTxMaxGasLimit; - emit NewPriorityTxMaxGasLimit(oldPriorityTxMaxGasLimit, _newPriorityTxMaxGasLimit);
file: contracts/zksync/facets/Admin.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Admin.sol#L88
memory
variable should created outside of loop, and get overriden with each iteration of loop, By doing so we save gas cost for memory variable creation in each iteration.uint256 processedLogs; bytes32 providedL2ToL1PubdataHash = keccak256(_newBatch.totalL2ToL1Pubdata); + address logSender; + uint256 logKey; + bytes32 logValue; // linear traversal of the logs 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); + (logSender, ) = UnsafeBytes.readAddress(emittedL2Logs, i + L2_LOG_ADDRESS_OFFSET); + (logKey, ) = UnsafeBytes.readUint256(emittedL2Logs, i + L2_LOG_KEY_OFFSET); + (logValue, ) = UnsafeBytes.readBytes32(emittedL2Logs, i + L2_LOG_VALUE_OFFSET); ..... .....
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L125-L127
+ bytes32 expectedUpgradeTxHash; for (uint256 i = 0; i < _newBatchesData.length; i = i.uncheckedInc()) { // The upgrade transaction must only be included in the first batch. - bytes32 expectedUpgradeTxHash = i == 0 ? _systemContractUpgradeTxHash : bytes32(0); + expectedUpgradeTxHash = i == 0 ? _systemContractUpgradeTxHash : bytes32(0);
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L243
+ bytes32 currentBatchCommitment; for (uint256 i = 0; i < committedBatchesLength; i = i.uncheckedInc()) { currentTotalBatchesVerified = currentTotalBatchesVerified.uncheckedInc(); require( _hashStoredBatchInfo(_committedBatches[i]) == s.storedBatchHashes[currentTotalBatchesVerified], "o1" ); - bytes32 currentBatchCommitment = _committedBatches[i].commitment; + currentBatchCommitment = _committedBatches[i].commitment;
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L337
+ uint256 commitBatchTimestamp; function executeBatches(StoredBatchInfo[] calldata _newBatchesData) external onlyValidator { uint256 delay = executionDelay; // uint32 unchecked { for (uint256 i = 0; i < _newBatchesData.length; ++i) { - uint256 commitBatchTimestamp = committedBatchTimestamp.get(_newBatchesData[i].batchNumber); + commitBatchTimestamp = committedBatchTimestamp.get(_newBatchesData[i].batchNumber);
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L117
L2ContractHelper.hashL2Bytecode(_factoryDeps[0])
could used directly instead of caching it memory, as it only used oncel2TokenProxyBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[2]); l2TokenBeacon = _l2TokenBeacon; - bytes32 l2BridgeImplementationBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[0]); bytes32 l2BridgeProxyBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[1]); // Deploy L2 bridge implementation contract address bridgeImplementationAddr = BridgeInitializationHelper.requestDeployTransaction( zkSync, _deployBridgeImplementationFee, - l2BridgeImplementationBytecodeHash, // @audit no need to cacghe + L2ContractHelper.hashL2Bytecode(_factoryDeps[0]); "", // Empty constructor data _factoryDeps // All factory deps are needed for L2 bridge );
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L99
L2ContractHelper.hashL2Bytecode(_factoryDeps[2])
require(msg.value == _deployBridgeImplementationFee + _deployBridgeProxyFee, "fee"); - l2TokenProxyBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[2]); l2TokenBeacon = _l2TokenBeacon; bytes32 l2BridgeImplementationBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[0]); bytes32 l2BridgeProxyBytecodeHash = L2ContractHelper.hashL2Bytecode(_factoryDeps[1]); // Deploy L2 bridge implementation contract address bridgeImplementationAddr = BridgeInitializationHelper.requestDeployTransaction( zkSync, _deployBridgeImplementationFee, l2BridgeImplementationBytecodeHash, // @audit no need to cacghe "", // Empty constructor data _factoryDeps // All factory deps are needed for L2 bridge ); // Prepare the proxy constructor data bytes memory l2BridgeProxyConstructorData; { // Data to be used in delegate call to initialize the proxy bytes memory proxyInitializationParams = abi.encodeCall( IL2ERC20Bridge.initialize, - (address(this), l2TokenProxyBytecodeHash, _governor) // @audit direct use l2TokenProxyBytecodeHash + (address(this), L2ContractHelper.hashL2Bytecode(_factoryDeps[2]), _governor) // @audit direct use l2TokenProxyBytecodeHash ); l2BridgeProxyConstructorData = abi.encode(bridgeImplementationAddr, _governor, proxyInitializationParams); }
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L96 https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L117
amount
variable instead use _amount
through out functionfunction deposit( address _l2Receiver, address _l1Token, uint256 _amount, uint256 _l2TxGasLimit, uint256 _l2TxGasPerPubdataByte, address _refundRecipient ) public payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 l2TxHash) { require(_amount != 0, "2T"); // empty deposit amount - uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount); - require(amount == _amount, "1T"); // The token has non-standard transfer logic + require(amount == _depositFunds(msg.sender, IERC20(_l1Token), _amount), "1T"); // verify the deposit amount is allowed _verifyDepositLimit(_l1Token, msg.sender, _amount, false); - bytes memory l2TxCalldata = _getDepositL2Calldata(msg.sender, _l2Receiver, _l1Token, amount); + bytes memory l2TxCalldata = _getDepositL2Calldata(msg.sender, _l2Receiver, _l1Token, _amount); // If the refund recipient is not specified, the refund will be sent to the sender of the transaction. // Otherwise, the refund will be sent to the specified address. // If the recipient is a contract on L1, the address alias will be applied. address refundRecipient = _refundRecipient; // @audit no need to initialized here if (_refundRecipient == address(0)) { refundRecipient = msg.sender != tx.origin ? AddressAliasHelper.applyL1ToL2Alias(msg.sender) : msg.sender; } l2TxHash = zkSync.requestL2Transaction{value: msg.value}( l2Bridge, 0, // L2 msg.value l2TxCalldata, _l2TxGasLimit, // @audit absence of check against gas _l2TxGasPerPubdataByte, new bytes[](0), refundRecipient ); // Save the deposited amount to claim funds on L1 if the deposit failed on L2 - depositAmount[msg.sender][_l1Token][l2TxHash] = amount; + depositAmount[msg.sender][_l1Token][l2TxHash] = _amount; - emit DepositInitiated(l2TxHash, msg.sender, _l2Receiver, _l1Token, amount); + emit DepositInitiated(l2TxHash, msg.sender, _l2Receiver, _l1Token, _amount); }
require
statement should be at top to save excess gas cost duringrequire(newTotalBatchesExecuted <= s.totalBatchesVerified, "n")
should be at top of executeBatches()
function executeBatches(StoredBatchInfo[] calldata _batchesData) external nonReentrant onlyValidator { uint256 nBatches = _batchesData.length; + uint256 newTotalBatchesExecuted = s.totalBatchesExecuted + nBatches; + require(newTotalBatchesExecuted <= s.totalBatchesVerified, "n"); for (uint256 i = 0; i < nBatches; i = i.uncheckedInc()) { _executeOneBatch(_batchesData[i], i); emit BlockExecution(_batchesData[i].batchNumber, _batchesData[i].batchHash, _batchesData[i].commitment); } - uint256 newTotalBatchesExecuted = s.totalBatchesExecuted + nBatches; s.totalBatchesExecuted = newTotalBatchesExecuted; - require(newTotalBatchesExecuted <= s.totalBatchesVerified, "n");
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L300
memory
variable should used instead state variable while emiting eventsfunction revertBatches(uint256 _newLastBatch) external nonReentrant onlyValidator { require(s.totalBatchesCommitted > _newLastBatch, "v1"); // The last committed batch is less than new last batch require(_newLastBatch >= s.totalBatchesExecuted, "v2"); // Already executed batches cannot be reverted if (_newLastBatch < s.totalBatchesVerified) { s.totalBatchesVerified = _newLastBatch; } s.totalBatchesCommitted = _newLastBatch; // Reset the batch number of the executed system contracts upgrade transaction if the batch // where the system contracts upgrade was committed is among the reverted batches. if (s.l2SystemContractsUpgradeBatchNumber > _newLastBatch) { delete s.l2SystemContractsUpgradeBatchNumber; } - emit BlocksRevert(s.totalBatchesCommitted, s.totalBatchesVerified, s.totalBatchesExecuted); + emit BlocksRevert(_newLastBatch, s.totalBatchesVerified, s.totalBatchesExecuted);
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L413
verificationKeyHash()
load all variables to memory via _loadVerificationKey()
via assembly where it only use VK_GATE_SETUP_0_X_SLOT
and VK_RECURSIVE_FLAG_SLOT
. Its recommended to load only these 2 to memory and use them.function verificationKeyHash() external pure returns (bytes32 vkHash) { _loadVerificationKey(); // @audit no need to update whole memory assembly { let start := VK_GATE_SETUP_0_X_SLOT let end := VK_RECURSIVE_FLAG_SLOT let length := add(sub(end, start), 0x20) vkHash := keccak256(start, length) } }
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/Verifier.sol#L261
genesisIndexRepeatedStorageChanges
consumes 8bytes, so moving it up/down a extra storage slot can be saved
struct InitializeData { IVerifier verifier; address governor; address admin; + uint64 genesisIndexRepeatedStorageChanges; bytes32 genesisBatchHash; - uint64 genesisIndexRepeatedStorageChanges; // @audit reorder bytes32 genesisBatchCommitment; IAllowList allowList; VerifierParams verifierParams; bool zkPorterIsAvailable; bytes32 l2BootloaderBytecodeHash; bytes32 l2DefaultAccountBytecodeHash; uint256 priorityTxMaxGasLimit; }
function _verifyDepositLimit(address _depositor, uint256 _amount) internal { IAllowList.Deposit memory limitData = IAllowList(s.allowList).getTokenDepositLimitData(address(0)); if (!limitData.depositLimitation) return; + uint256 res = s.totalDepositedAmountPerUser[_depositor] + _amount; - require(s.totalDepositedAmountPerUser[_depositor] + _amount <= limitData.depositCap, "d2"); - s.totalDepositedAmountPerUser[_depositor] += _amount; + require(res <= limitData.depositCap, "d2"); + s.totalDepositedAmountPerUser[_depositor] = res;
file: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L279-L280
} else { + uint256 res = totalDepositedAmountPerUser[_l1Token][_depositor] + _amount; - require(totalDepositedAmountPerUser[_l1Token][_depositor] + _amount <= limitData.depositCap, "d1"); - totalDepositedAmountPerUser[_l1Token][_depositor] += _amount; + require(res + _amount <= limitData.depositCap, "d1"); + totalDepositedAmountPerUser[_l1Token][_depositor] = res; }
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L347-L348
totalBatchesVerified
and totalBatchesCommitted
could packed together to save save extra storage slotaddress governor; + bool zkPorterIsAvailable; /// @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; + uint256 totalBatchesExecuted; + uint256 totalBatchesVerified; /// @notice Total number of committed batches i.e. batches[totalBatchesCommitted] points at the latest committed /// batch uint256 totalBatchesCommitted; 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; // @audit re-order /// @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;
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/Storage.sol#L116 https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/Storage.sol#L94 https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/Storage.sol#L97
baseCost
variable, instead use expression directly inside require()
{ params.l2GasPrice = _isFree ? 0 : _deriveL2GasPrice(tx.gasprice, _l2GasPerPubdataByteLimit); - uint256 baseCost = params.l2GasPrice * _l2GasLimit; - require(msg.value >= baseCost + _l2Value, "mv"); + require(msg.value >= params.l2GasPrice * _l2GasLimit + _l2Value, "mv");
file: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L305-L306
value + 1
should be replace with value++
. In some places `uncheck{ value++ } will be more accurate.- require(_newBatch.batchNumber == _previousBatch.batchNumber + 1, "f"); + require(_newBatch.batchNumber == _previousBatch.batchNumber++, "f");
Instances()
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L32
require(currentBatchNumber == s.totalBatchesExecuted + _executedBatchIdx + 1, "k");
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L275
msg.sender
should done using assembly
modifier onlyGovernor() { require(msg.sender == s.governor, "1g")
File: ethereum/contracts/zksync/facets/Base.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Base.sol#L19
modifier onlyGovernorOrAdmin() { require(msg.sender == s.governor || msg.sender == s.admin, "Only by governor or admin"); _; }
File: ethereum/contracts/zksync/facets/Base.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Base.sol#L25
modifier onlyValidator() { require(s.validators[msg.sender], "1h"); // validator is not active _; }
File: ethereum/contracts/zksync/facets/Base.sol https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Base.sol#L30
modifier onlyValidator() { require(msg.sender == validator, "8h"); _; }
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L66
assert
assert(block.chainid != 1);
File: https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L337
assert(L2_TO_L1_LOG_SERIALIZE_SIZE != 2 * 32);
#0 - 141345
2023-10-26T04:31:38Z
1 L 2 r 7 nc
[Gas-01] emit could used differently to save gas r
[Gas-02] memory variable should created outside of loop nc
[Gas-03] No need to cache state variable which only used once nc
[Gas-04] Some require statement should be at top nc
[Gas-05] memory variable should used instead state variable while emiting events L
[Gas-06] verificationKeyHash() load all variables to memory r
[Gas-07] struct can be packed more efficiently d
[Gas-08] nc
[Gas-09] Extra storage slot can save in state variable case d
[Gas-10] No need to create baseCost variable nc
[Gas-11] value + 1 should be replace with value++ nc
[Gas-12] Validation check for msg.sender should done using assembly nc
[Gas-13] Use of assert i
#1 - c4-pre-sort
2023-11-02T16:09:13Z
141345 marked the issue as sufficient quality report
#2 - c4-judge
2023-11-15T13:15:21Z
GalloDaSballo marked the issue as grade-b
#3 - c4-judge
2023-12-10T19:07:46Z
GalloDaSballo marked the issue as grade-a