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: 19/64
Findings: 1
Award: $3,839.27
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: zkLotus
Also found by: K42, Sathish9098, catellatech, peanuts, pfapostol
3839.2698 USDC - $3,839.27
This audit is conducted with a cognizant understanding of the limitations imposed by the available audit time, which proved to be insufficient for a comprehensive examination of the entire codebase, amounting to 31,029 Source Lines of Code (SLOC). To optimize the auditing process, a strategic decision was made to prioritize the assessment of L1 contracts and their intricate interactions with their corresponding L2 counterparts. This focused approach allows for a meticulous examination of critical components within the given constraints, ensuring a thorough evaluation of the specified smart contract system's key elements.
This analysis report aims to provide a comprehensive overview of the smart contract system, specifically focusing on the L1 contracts: ExecutorFacet
, MailboxFacet
, AdminFacet
, Diamond
, TransactionValidator
, ValidatorTimelock
, L1ERC20Bridge
, and the communication patterns between L1ERC20Bridge
and L2ERC20Bridge
.
Commencing the assessment, I initiated a thorough examination of the available documentation. Subsequently, I delineated the specific areas that warranted in-depth scrutiny during the audit, focusing particularly on L1 contracts and the intricacies of the L1 to L2 bridges.
Upon a comprehensive review of the code encompassing all facets and the diamond proxy architecture, I directed my attention towards a meticulous examination of the L1 ERC20 and L1 WETH bridges.
An integral aspect of this process involved the compilation of a comprehensive glossary, documenting terms that were either unfamiliar or not entirely elucidated. Subsequently, I embarked on an extensive study of these terms, utilizing both official documentation and external sources, including Google, to augment my understanding.
Following the attainment of a holistic understanding of L1 contracts, I proceeded to the identification of potential risks and the exploration of quality assurance (QA) and gas-related issues.
Throughout the audit, I refrained from utilizing specific programs. However, for the purpose of report compilation, I employed chatGPT with the prompt: "Rewrite the segments of the web3 audit report provided, imbuing them with a more professional style and adhering to grammatical precision." (Because my English writing skills are not as good as reading and listening.)
The prioritization of specific contracts during the audit was driven by a strategic focus on areas exhibiting lower test coverage percentages. This deliberate choice was guided by several considerations aimed at enhancing the overall robustness and reliability of the codebase.
Certain contracts, such as cache/solpp-generated-contracts/bridge/L1ERC20Bridge.sol
and cache/solpp-generated-contracts/zksync/facets/Admin.sol
, exhibited lower coverage percentages. These contracts are integral components of the system, and addressing coverage gaps in these critical areas was essential to mitigate risks and ensure the robustness of the entire codebase.
File | % Lines | % Statements | % Branches | % Funcs |
---|---|---|---|---|
cache/solpp-generated-contracts/bridge/L1ERC20Bridge.sol | 33.33% (22/66) | 38.20% (34/89) | 9.38% (3/32) | 45.45% (5/11) |
cache/solpp-generated-contracts/bridge/L1WethBridge.sol | 75.00% (36/48) | 72.41% (42/58) | 67.86% (19/28) | 85.71% (6/7) |
cache/solpp-generated-contracts/bridge/libraries/BridgeInitializationHelper.sol | 0.00% (0/3) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/1) |
cache/solpp-generated-contracts/common/AllowList.sol | 100.00% (26/26) | 100.00% (31/31) | 75.00% (9/12) | 100.00% (9/9) |
cache/solpp-generated-contracts/common/ReentrancyGuard.sol | 0.00% (0/3) | 0.00% (0/3) | 0.00% (0/2) | 0.00% (0/1) |
cache/solpp-generated-contracts/common/libraries/L2ContractHelper.sol | 50.00% (7/14) | 42.11% (8/19) | 30.00% (3/10) | 25.00% (1/4) |
cache/solpp-generated-contracts/common/libraries/UncheckedMath.sol | 0.00% (0/2) | 0.00% (0/4) | 100.00% (0/0) | 0.00% (0/2) |
cache/solpp-generated-contracts/common/libraries/UnsafeBytes.sol | 100.00% (8/8) | 100.00% (8/8) | 100.00% (0/0) | 100.00% (4/4) |
cache/solpp-generated-contracts/zksync/DiamondInit.sol | 0.00% (0/17) | 0.00% (0/18) | 0.00% (0/10) | 0.00% (0/1) |
cache/solpp-generated-contracts/zksync/DiamondProxy.sol | 100.00% (6/6) | 100.00% (7/7) | 50.00% (3/6) | 100.00% (1/1) |
cache/solpp-generated-contracts/zksync/ValidatorTimelock.sol | 0.00% (0/18) | 0.00% (0/25) | 0.00% (0/2) | 0.00% (0/8) |
cache/solpp-generated-contracts/zksync/Verifier.sol | 0.00% (0/3) | 0.00% (0/3) | 100.00% (0/0) | 0.00% (0/3) |
cache/solpp-generated-contracts/zksync/facets/Admin.sol | 21.05% (8/38) | 25.00% (10/40) | 30.00% (3/10) | 30.00% (3/10) |
cache/solpp-generated-contracts/zksync/facets/Executor.sol | 87.50% (119/136) | 87.10% (162/186) | 76.00% (76/100) | 90.00% (18/20) |
cache/solpp-generated-contracts/zksync/facets/Getters.sol | 22.00% (11/50) | 21.54% (14/65) | 25.00% (1/4) | 20.59% (7/34) |
cache/solpp-generated-contracts/zksync/facets/Mailbox.sol | 58.67% (44/75) | 58.82% (60/102) | 25.00% (7/28) | 50.00% (8/16) |
cache/solpp-generated-contracts/zksync/libraries/Diamond.sol | 98.85% (86/87) | 99.07% (106/107) | 79.55% (35/44) | 100.00% (10/10) |
cache/solpp-generated-contracts/zksync/libraries/LibMap.sol | 0.00% (0/9) | 0.00% (0/14) | 100.00% (0/0) | 0.00% (0/2) |
cache/solpp-generated-contracts/zksync/libraries/Merkle.sol | 0.00% (0/10) | 0.00% (0/12) | 0.00% (0/6) | 0.00% (0/2) |
cache/solpp-generated-contracts/zksync/libraries/PriorityQueue.sol | 0.00% (0/14) | 0.00% (0/16) | 0.00% (0/4) | 0.00% (0/7) |
cache/solpp-generated-contracts/zksync/libraries/TransactionValidator.sol | 71.05% (27/38) | 77.08% (37/48) | 13.33% (4/30) | 80.00% (4/5) |
My primary emphasis during the analysis centered on the examination of L1 contracts and the L1->L2 bridges within the codebase. Understanding the intricacies of these components is crucial for ensuring the seamless operation and security of the entire system.
To illustrate the deposit transaction process from L1 to L2, the following diagram provides a visual representation:
Note: To view the image more closely, open it in a new tab as GitHub supports zooming.
Then transaction is written in the Priority Queue; Understanding the mechanics of the Priority Queue is vital for comprehending the transaction processing workflow. The following diagram provides a concise representation:
Public functions:
setPendingGovernor
: -
s.pendingGovernor
update with inputonlyGovernor
s.pendingGovernor
acceptGovernor
s.governor
update with pendings.pendingGovernor
setPendingAdmin
s.pendingAdmin
update with inputonlyGovernorOrAdmin
s.pendingAdmin
acceptAdmin
s.admin
update with pendings.pendingAdmin
setValidator
s.validators[_validator]
onlyGovernorOrAdmin
setPorterAvailability
s.zkPorterIsAvailable
onlyGovernor
setPriorityTxMaxGasLimit
s.priorityTxMaxGasLimit
onlyGovernor
executeUpgrade
:
onlyGovernor
Diamond.diamondCut
freezeDiamond
:
diamondStorage.isFrozen
trueonlyGovernor
unfreezeDiamond
:
diamondStorage.isFrozen
falseonlyGovernorOrAdmin
External functions:
commitBatches
: -
s.storedBatchHashes[_lastCommittedBatchData.batchNumber]
updated with hashs.l2SystemContractsUpgradeBatchNumber
updated with batch numbers.storedBatchHashes[_lastCommittedBatchData.batchNumber]
updated with hashs.totalBatchesCommitted
incremented by number of new added batchesnonReentrant
onlyValidator
totalBatchesCommitted
point to the last data provided as input_commitBatchesWithoutSystemContractsUpgrade
_commitBatchesWithSystemContractsUpgrade
totalBatchesCommitted
with length of new batchesexecuteBatches
: -
s.priorityQueue
poped for every executed batchs.l2LogsRootHashes[currentBatchNumber]
updated with stored log tree hashs.totalBatchesExecuted
incremented with amount executeds.l2SystemContractsUpgradeTxHash
s.l2SystemContractsUpgradeBatchNumber
nonReentrant
onlyValidator
_executeOneBatch
BlockExecution
totalBatchesExecuted
incremented by number of executed batchesl2SystemContractsUpgradeTxHash
and l2SystemContractsUpgradeBatchNumber
proveBatches
: -
s.totalBatchesVerified
incremented by number of newly verified batchesnonReentrant
onlyValidator
_prevBatch
indeed previous verified batch_getBatchProofPublicInput
verifier.verify
BlocksVerification
totalBatchesVerified
revertBatches
: -
s.totalBatchesVerified
update with batch to reverts.totalBatchesCommitted
update with batch to reverts.l2SystemContractsUpgradeBatchNumber
nonReentrant
onlyValidator
totalBatchesVerified
if batch to revert is verifiedtotalBatchesCommitted
l2SystemContractsUpgradeBatchNumber
BlocksRevert
Internal function :
_commitOneBatch
: -
_processL2Logs
previousBatchHash
expectedPriorityOperationsHash
expectedNumberOfLayer1Txs
_verifyBatchTimestamp
_createBatchCommitment
StoredBatchInfo
with created commitment_verifyBatchTimestamp
: - Verifies correctness of the new batch and the new L2 block timestamps_processL2Logs
: -
systemLogs
:
logSender
, logKey
, logValue
logKey
was not processed before_commitBatchesWithoutSystemContractsUpgrade
: -
_commitOneBatch
_hashStoredBatchInfo
_hashStoredBatchInfo
to storedBatchHashes
[_lastCommittedBatchData.batchNumber
]BlockCommit
_commitBatchesWithSystemContractsUpgrade
: -
l2SystemContractsUpgradeBatchNumber
- number of the first batch where upgrade happened_commitOneBatch
_hashStoredBatchInfo
_hashStoredBatchInfo
to storedBatchHashes
[_lastCommittedBatchData.batchNumber
]BlockCommit
_collectOperationsFromPriorityQueue
:
priorityOp
from queuecanonicalTxHash
_executeOneBatch
:
_collectOperationsFromPriorityQueue
priorityOperationsHash
match expectedl2LogsRootHashes
from batch to correspondent index_getBatchProofPublicInput
:
_prevBatchCommitment
, _currentBatchCommitment
, recursionNodeLevelVkHash
, recursionLeafLevelVkHash
_maxU256
_createBatchCommitment
: -
_batchPassThroughData
_batchMetaParameters
_batchAuxiliaryOutput
_batchPassThroughData
: - Encode packed indexRepeatedStorageChanges
,newStateRoot
, 0 ,0_batchMetaParameters
: - Encode packed zkPorterIsAvailable
,l2BootloaderBytecodeHash
,l2DefaultAccountBytecodeHash
_batchAuxiliaryOutput
: -
systemLogs
l2ToL1LogsHash
, _stateDiffHash
,bootloaderHeapInitialContentsHash
,eventsQueueStateHash
_hashStoredBatchInfo
: - keccak256
of encoded struct_checkBit
: - check if bit at index is 1_setBit
: - set bit at index to 1Public functions:
proveL2MessageInclusion
: -
_L2MessageToLog
_proveL2LogInclusion
proveL2LogInclusion
: -
_proveL2LogInclusion
proveL1ToL2TransactionStatus
:
L2Log
struct_proveL2LogInclusion
l2TransactionBaseCost
: -
_deriveL2GasPrice
_l2GasLimit
finalizeEthWithdrawal
: -
s.isEthWithdrawalFinalized[_l2BatchNumber][_l2MessageIndex]
set to true
this
to _l1WithdrawReceiver
nonReentrant
senderCanCallFunction(s.allowList)
L2Message
struct_parseL2WithdrawalMessage
proveL2MessageInclusion
isEthWithdrawalFinalized
_withdrawFunds
EthWithdrawalFinalized
requestL2Transaction
: -
depositLimitation
is set to true
:
s.totalDepositedAmountPerUser[_depositor]
incremented by amounts.priorityQueue
- new operation addednonReentrant
senderCanCallFunction(s.allowList)
msg.sender
≠msg.origin
call AddressAliasHelper.applyL1ToL2Alias
(apply offset)_verifyDepositLimit
_requestL2Transaction
Internal functions:
_withdrawFunds
: - Low level call with amount_proveL2LogInclusion
: -
calculateRoot
_L2MessageToLog
: - convert L2Message
to L2Log
_deriveL2GasPrice
: - calculate gas price using prepocessed constants and user input_verifyDepositLimit
: -
IAllowList(s.allowList).getTokenDepositLimitData(address(*0*))
totalDepositedAmountPerUser
_requestL2Transaction
: -
msg.value
cover gas cost and l2value
AddressAliasHelper.applyL1ToL2Alias
WritePriorityOpParams
and call _writePriorityOp
_serializeL2Transaction
: -
L2CanonicalTransaction
struct_writePriorityOp
: -
_serializeL2Transaction
TransactionValidator.validateL1ToL2Transaction
NewPriorityRequest
_hashFactoryDeps
: -
_parseL2WithdrawalMessage
: -
fsig
finalizeEthWithdrawal
l1Receiver
and amount
getDiamondStorage
: - storage refdiamondCut
: - main entry point
_addFunctions
_replaceFunctions
_removeFunctions
_initializeDiamondCut
DiamondCut
_addFunctions
:
_saveFacetIfNew
_addOneFunction
_replaceFunctions
:
_removeOneFunction
_saveFacetIfNew
_addOneFunction
_removeFunctions
:
_removeOneFunction
_saveFacetIfNew
:
ds.facetToSelectors[_facet].facetPosition
ds.facets
_addOneFunction
:
ds.selectorToFacet[_selector]
ds.facetToSelectors[_facet].selectors
_removeOneFunction
:
ds.facetToSelectors[_facet].selectors
pop with reorderingds.selectorToFacet[_selector]
_removeFacet
_removeFacet
:
ds.facets
pop with reordering_initializeDiamondCut
:
init
address is zero:
Public:
initialize
: -
l2TokenProxyBytecodeHash
l2TokenBeacon
reentrancyGuardInitializer
msg.value
l2TokenProxyBytecodeHash
to hashL2Bytecode
of _factoryDeps[*2*]
l2TokenBeacon
deposit(933999fb)
: - the same as e8b99b1b
but _refundRecipient
is address zero
deposit(e8b99b1b)
: -
totalDepositedAmountPerUser
depositAmount[**msg.sender**][_l1Token][l2TxHash]
nonReentrant
senderCanCallFunction(allowList)
_depositFunds
_verifyDepositLimit
_getDepositL2Calldata
zkSync.requestL2Transaction
depositAmount[**msg.sender**][_l1Token][l2TxHash]
claimFailedDeposit
: -
nonReentrant
senderCanCallFunction(allowList)
zkSync.proveL1ToL2TransactionStatus
finalizeWithdrawal
:
l2TokenAddress
: -
L2ContractHelper.computeCreate2Address
Internal:
_depositFunds
: -
_getDepositL2Calldata
: -
_getERC20Getters
IL2Bridge.finalizeDeposit
and parameters_getERC20Getters
: -
_parseL2WithdrawalMessage
_verifyDepositLimit
: -
totalDepositedAmountPerUser
totalDepositedAmountPerUser
The Diamond framework uses a smart contract design called EIP-2535 diamond proxy. This design is flexible and allows different parts of the system to be upgraded independently. It's like having multiple pieces that can be upgraded without affecting the whole system.
Apart from the Diamond framework, there's a system for moving assets between L1 and L2. It's like a bridge connecting two islands.
L1ERC20Bridge
: Deals with regular ERC20 tokens, managing their movement between L1 and L2.L2ERC20Bridge
: The L2 counterpart, handling the other side of ERC20 token movement.L1WethBridge
: Specialized in handling WETH token transfers between L1 and L2 for a smoother experience.L2WethBridge
: The L2 version, ensuring WETH can move seamlessly between the two layers.This acts as a time delay mechanism, giving validators a window to review and approve batch transactions before they're executed. It's like a safety lock to prevent hasty decisions.
Two types of transactions can be initiated on L1:
To initiate a priority operation, the requestL2Transaction
method on L1 is called. This method performs checks to ensure the transaction is processable and provides sufficient fees. The transaction is then appended to the priority queue.
When an operator identifies a priority operation, it can include the transaction in the batch. For L1→L2 transactions, there is no signature verification, so the bootloader maintains two variables: numberOfPriorityTransactions and priorityOperationsRollingHash. These variables help ensure that only valid transactions are included.
At the end of the batch, two L2→L1 log system logs are submitted with these values.
Overall, the codebase is crafted with a high-level approach. However, the scrutiny has unearthed certain deficiencies, encompassing inaccurate comments, improper utilization of NatSpec tags, and various minor issues elucidated in the Quality Assurance (QA) report. These aspects warrant attention for a more precise and polished implementation.
I would like to reiterate that the Quality Assurance (QA) report exclusively covers L1 contracts. This focused scope is deliberate, intended to prevent the inundation of tasks with substantial complexity.
It's pertinent to note that conventional centralization concerns often associated with patterns like "onlyOwner" are not directly applicable to ZkSync. ZkSync stands out as a distinct Ethereum chain designed with a primary focus on decentralized principles and privacy enhancement through the strategic application of zero-knowledge proofs.
In the ZkSync ecosystem, validators play a crucial role by having the capability to approve transactions. Their involvement is pivotal for the system's operation. Additionally, the administration retains the authority to initiate upgrade transactions. However, it's essential to underscore that such upgrades are contingent on the consensus achieved through a multisignature process.
This multisignature requirement ensures that no single entity, even within the administration, can unilaterally implement upgrades. It acts as a robust safeguard against undue concentration of power, aligning with the overarching ethos of decentralization that ZkSync prioritizes. Therefore, from the standpoint of centralization, the implemented measures and governance structure contribute to the project's resilience and integrity.
Being relatively new to the realm of Layer 2 (L2) solutions, my introduction to ZkSync marked an exploration into uncharted territory. The intricacies of ZkSync's functionality, coupled with the novel concept of zero-knowledge proofs, presented a steep learning curve for me. My focus encompassed delving into the initiation of transactions within the Ethereum layer, understanding the role of validators in validating transaction legitimacy, and unraveling the process of aggregating transactions into batches on Layer 1 (L1). This learning journey provided valuable insights into the underlying mechanisms of ZkSync and broadened my understanding of transactional workflows in the Ethereum ecosystem.
74 hours
#0 - c4-pre-sort
2023-11-02T15:58:03Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-11-21T16:50:30Z
GalloDaSballo marked the issue as grade-a