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: 33/64
Findings: 2
Award: $990.53
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: lsaudit
Also found by: 0xAnah, 0xhacksmithh, 0xsomeone, SM3_SS, Sathish9098, albahaca, c3phas, hunter_w3b
213.7261 USDC - $213.73
## Issues
Struct Packing
uint256(1)
/uint256(2)
for gas optimizationtotalDepositedAmountPerUser[_l1Token][_depositor]
should be cachedGlogtopic
Gas Savings!_checkBit(processedLogs, uint8(logKey)
this check is redundant when processedLogs
is 0claimFailedDeposit()
function can be refactored for greater gas efficiencyfallback()
function for better gas efficiencycommitBatches()
function for better gas efficiencyrequestL2Transaction()
function for better gas efficiencyunnecessary GAS
immutable
variable incurs extra GasBytes
constants are more efficient than string
constantscache variable
only used once
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.
protocolVersion
and l2SystemContractsUpgradeBatchNumber
can be uint96
instead of uint256
: Saves 4000 GAS
, 2 SLOT
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; }
newProtocolVersion
can be uint96
instead of uint256
: Saves 2000 GAS
, 1 SLOT
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; }
txType
and nonce
can be uint128 instead of uint256 : Saves 2000 GAS
, 1 SLOT
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; }
uint256(1)
/uint256(2)
for gas optimization100000 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;
FILE: 2023-10-zksync/code/contracts/ethereum/contracts/zksync/libraries/Diamond.sol 32: bool isFreezable; 53: bool isFrozen;
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;
## [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
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: }
Glogtopic
Gas SavingsWe 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);
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;
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) }
We have a few methods we can use when assigning values to struct
135 GAS
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 ;
!_checkBit(processedLogs, uint8(logKey)
this check is redundant when processedLogs
is 0The 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");
claimFailedDeposit()
function can be refactored for greater gas efficiencyGas 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
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");
fallback()
function for better gas efficiencydiamondStorage
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
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");
commitBatches()
function for better gas efficiencyThe 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
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");
requestL2Transaction()
function for better gas efficiencyPrioritize the check of the _l2GasPerPubdataByteLimit
parameter before performing any other operations, resulting in a savings of 200 GAS
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");
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.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); }
block.timestamp - COMMIT_TIMESTAMP_NOT_OLDER <= batchTimestamp
check then caching lastL2BlockTimestamp value unnecessary and waste of gasFILE: 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);
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()) {
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)
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";
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";
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";
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";
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";
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.
hasSpecialAccessToCall[_caller][_target][_functionSig]
can be used directly instead of local cacheFILE: 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); }
s.priorityQueue.popFront()
is redundant and costs 250 GAS
per iteration. Instead, use the result of the function call directly.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
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
#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.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
totalDepositedAmountPerUser[_l1Token][_depositor] should be cached : Saves 100 GAS , 1 SLOD
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.
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.
From Issue https://github.com/code-423n4/2023-10-zksync-findings/issues/924
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
๐ Selected for report: zkLotus
Also found by: K42, Sathish9098, catellatech, peanuts, pfapostol
776.8033 USDC - $776.80
List | Head | Details |
---|---|---|
1 | Overview of zkSync Era | Description overview of zkSync Era Protocol |
2 | Code review approach and methodology | Understand the purpose of the code |
3 | Analysis of the code base | To evaluate the quality, design, and implementation of the code |
4 | Mechanism Review | Thorough examination, assessment, or evaluation of a mechanism |
5 | Architecture Overview of Zksync Era | Project's architecture and features |
6 | Systemic & Centralization Risks | Ensure that the system remains robust against systemic and centralization risks |
7 | Competition analysis | Other Competitors for Zksyc |
8 | Other Recommendations | General Recommendations |
9 | New insights and learning from this audit | Learnings from this audit |
10 | Time spent | The total time spend for this analysis |
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
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
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.
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
.
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.
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.
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
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
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
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; }
function proveL2MessageInclusion( uint256 _batchNumber, uint256 _index, L2Message memory _message, bytes32[] calldata _proof ) public view returns (bool) { return _proveL2LogInclusion(_batchNumber, _index, _L2MessageToLog(_message), _proof); }
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 ); }
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" ); }
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); }
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; }
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); }
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)); }
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
Zero-Knowledge Rollups
, are a technology used in the Ethereum ecosystem to enhance the scalability
and efficiency
of the network.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.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 .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 transactionto_l1
- Sends a system L2โL1 log to Ethereumevent
- Emits an L2 log to zkSyncprecompile_call
- This is an opcode that accepts two parameters: the uint256 representing the packed parameters for it as well as the ergs to burn.ecrecove
r system contract, it performs the ecrecover operationsha256/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 VMOpcodes that can be generally accessed by any contract
near_call
- framed
jump to some location of the code of your contractgetMeta
- Returns an u256
packed value of ZkSyncMeta struct
getCodeAddress
- Receives the address of the executed coder1
- The pointer to the calldatar2
- 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
flag1-st bit
: isSystem
flagAccess Control
OnlyGovernor
role and onlyGovernorOrAdmin
is responsible for managing the system's parameters.function setPendingGovernor(address _newPendingGovernor) external onlyGovernor { function setPendingAdmin(address _newPendingAdmin) external onlyGovernorOrAdmin {
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
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.
The Hyperchains architecture
is complex
, and it may be difficult for developers
to understand and use
.
The governance of Hyperchains is unclear. This could lead to disputes between Hyperchains that could be difficult to resolve.
Hyperbridges are used to connect Hyperchains
together. However, Hyperbridges
could be used to create malicious Hyperchains
that could harm the Hyperchains ecosystem
.
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.
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
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.
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).
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.
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
.
The proof generation
process can be computationally expensiv
e. This can limit the throughput
of the zkSync Era network
.
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.
zkSync Era allows developers to build projects using the same programming languages and tools used to build on Ethereum
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 settlemen
t: 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.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.
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
The Hyperchains architecture is a centralized architecture. This means that a small number of actors have a large amount of control over the network
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.
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.
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.
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.
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.
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.
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.
Systemic Risk
:
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.
Systemic Risk
:
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.(s.priorityTxMaxGasLimit)
. Inadequate estimation of gas limits could lead to transaction failures or reordering issues.Systemic Risk
:
OpenZeppelin
. Systemic risks can emerge if vulnerable versions are usedvalidateL1ToL2Transaction
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 vulnerabilitiesgetOverheadForTransaction
function, especially the calculations related to overhead, tx
slot overhead, and overhead for gas.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
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.
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.
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
.
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
Systemic Risk
:
zkSync
and _l2TokenBeacon
. It's essential to ensure that these addresses are accurateL2 bridge
implementation and L2 bridge proxy
. Any discrepancies in these fees can lead to deployment failures.nonReentrant
modifier is used to prevent reentrancy attacks. Ensure that it effectively protects the contract from reentrancy risks in all relevant functionsdeposit
methods can be confusing for usersSystemic Risk
:
L2 tokens
and token proxies
. Ensure that the proxy deployment mechanism is secure and properly configured to prevent unauthorized contract creationbridgeMint
and bridgeBurn
functions in the L2StandardToken are implemented correctly, as they are crucial to deposit and withdrawal functionality.L1 token
addresses to their L2 counterparts
. Ensure that this mapping is accurate and properly maintained to avoid discrepancies between L1 and L2 token addressessendMessageToL1
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.bridgeMint
, bridgeBurn
, and sendMessageToL1
, are protected against reentrancy attacksSystemic Risk
:
_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 corruptiondictionary 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._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
Systemic Risk
:
pubdata
, including state diffs, Merkle trees, and more. Any errors or discrepancies in the verification process could lead to incorrect data being sent to L1reentrancy attacks
and carefully validate data from untrusted sources
Cenralization Risks
:
sendL2ToL1Log()
is with Administrator control
Competitor | Description | Advantages | Disadvantages |
---|---|---|---|
Optimism | Optimistic rollup | Fast withdrawals, low fees | Fraud proofs can be slow |
Arbitrum | Optimistic rollup | Fast withdrawals, low fees | Fraud proofs can be slow |
StarkNet | zk-rollup | High throughput, low fees | Limited ecosystem |
Immutable X | zk-rollup | Designed for NFTs, high throughput | Limited ecosystem |
Metis | zk-rollup | EVM-compatible, low fees | Limited ecosystem |
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 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.
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.
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
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
.
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)
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";
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.
50 hours
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