xanoutas/solidity-security-findings huggingface dataset:
[H] Selector positions are not updated when removing them from the selectors array
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The Diamond.sol contract is a key component of the Diamond Standard, allowing for dynamic and upgradeable smart contracts.

The ```_removeOneFunction``` is designed to to remove a single associated function (selector) 
from a specific facet within the Diamond smart contract.

The function locates the facet associated with the given selector, identifies the position of the selector 
in the facet's array, and the position of the last selector in that array. If the selector isn't the last one,
it replaces the selector to be removed with the last selector in the array.

Then, it removes the last selector from the array and cleans up the association with the selector.
## Proof of Concept
However the vulnerability comes from the fact that the selector positions are not updated when removing them 
from the selectors array, especially if its the last selector in the array being removed. In scenarios where 
selectors have been removed but their positions haven't been accurately updated, the Diamond smart contract can maintain references to these selectors, even after they should have been removed.

```
// If the selector is not at the end of the array then move the last element to the selector position
if (selectorPosition != lastSelectorPosition) {
    bytes4 lastSelector = ds.facetToSelectors[_facet].selectors[lastSelectorPosition];

    ds.facetToSelectors[_facet].selectors[selectorPosition] = lastSelector;
    ds.selectorToFacet[lastSelector].selectorPosition = selectorPosition.toUint16();
}
```

Here, the code checks if ```selectorPosition``` is different from ```lastSelectorPosition``` before moving the last selector to the position of the removed selector and updating the position of the last selector. 

If ```selectorPosition``` is equal to ```lastSelectorPosition```, this part of the code is not executed.

In situations where a selector is the last element in the array, it should still update its position, 
but this code conditionally skips that update when it should not. 

```
 // Remove last element from the selectors array
        ds.facetToSelectors[_facet].selectors.pop();
```

The code then removes the last element from the selectors array, but the position of the removed selector is 
not updated. 


For example, if there are three selectors in the array with positions [0, 1, 2], and we want to remove the 
last selector at position 2, the code does the following:

It checks if ```selectorPosition``` (the position of the selector to be removed) is not the same as ```lastSelectorPosition``` (the position of the last selector in the array). If it's not the same, the code tries to replace the selector to be removed with the last selector in the array.

If ```selectorPosition``` is equal to ```lastSelectorPosition``` (which is the case
An attacker can control key parameters of contracts
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/upgrades/DefaultUpgrade.sol#L25-L46


# Vulnerability details

## Impact
The DefaultUpgrade.sol contract has an upgrade() function (https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/upgrades/DefaultUpgrade.sol#L25-L46 ). The upgrade() function is public and there is no protection as to who can call this function. Anyone can call her.

## Proof of Concept
Function upgrade() - the main function that will be called by the upgrade proxy. ProposedUpgrade is used as an input parameter:

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

The upgrade() function can change parameters such as:
_setNewProtocolVersion(_proposedUpgrade.newProtocolVersion);
_upgradeL1Contract(_proposedUpgrade.l1ContractsUpgradeCalldata);
_upgradeVerifier(_proposedUpgrade.verifier, _proposedUpgrade.verifierParams);
_setBaseSystemContracts(_proposedUpgrade.bootloaderHash, _proposedUpgrade.defaultAccountHash);
_setAllowList(IAllowList(_proposedUpgrade.newAllowList));
_postUpgrade(_proposedUpgrade.postUpgradeCalldata);

Changing these parameters by anyone may result in the contract not functioning correctly.

## Tools Used
Manual review

## Recommended Mitigation Steps
Remove direct changes to the above parameters from the upgrade() function.


## Assessed type

Access Control
revertBatches() can undo a batch that has already been executed
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Function Executor.revertBatches() - reverts unexecuted batches. The function receives _newLastBatch as an input parameter. _newLastBatch - batch number after which batches should be reverted.

## Proof of Concept
The revertBatches() function has a check:
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L400
require(_newLastBatch >= s.totalBatchesExecuted, "v2"); // Already executed batches cannot be reverted

The comment reads: “Already executed batches cannot be reverted.” However, the presence of equal allows you to cancel an already executed package. This will cause the system to malfunction.

## Tools Used
Manual review

## Recommended Mitigation Steps
Fix to require(_newLastBatch > s.totalBatchesExecuted, "v2")


## Assessed type

Context
There is no correspondence between which batches were proven and which were then executed
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The Executor.sol contract has the executeBatches() and proveBatches() functions.
executeBatches() - batch executes, complete priority operations and process withdrawals.
proveBatches() - batches commitment verification.
However, when executing executeBatches(), it is impossible to accurately determine that a given batch was proven.

## Proof of Concept
When executing the executeBatches() function, the only check whether a given batch has been proven is:
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L300
require(newTotalBatchesExecuted <= s.totalBatchesVerified, "n"); // Can't execute batches more than committed and proven currently.

However, based on this check, it cannot be said for sure that this batch was proven. In fact, the verification is carried out based on the number of batch proven.

This may result in the execution of executeBatches for which proveBatches has not been executed.

## Tools Used
Manual review

## Recommended Mitigation Steps
Add a mapping where you can record batch proven and check them when calling executeBatches().


## Assessed type

Invalid Validation
_verifyDepositLimit() does not take into account data when depositLimitation = false
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L188
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L278
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L340-L350


# Vulnerability details

## Impact
In the L1ERC20Bridge.sol contract, the _verifyDepositLimit() function is used to check verify the deposit amount is allowed:
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L340-L350

In the _verifyDepositLimit() function, the
if (!limitData.depositLimitation) return; // no deposit limitation is placed for this token

This means that if depositLimitation == false the function is terminated.

## Proof of Concept
Let's assume that first the depositLimitation parameter = true. The user calls the L1ERC20Bridge.deposit() function, where the parameter
totalDepositedAmountPerUser[_l1Token][_depositor] += _amount;
increases by amount. Let us assume that amount is close to the depositCap parameter.

The admins then decide to set depositLimitation = false for this token. At this time, the user calls the L1ERC20Bridge.claimFailedDeposit() function. However, its totalDepositedAmountPerUser parameter is not reduced by amount, because there is a return in the line:
if (!limitData.depositLimitation) return; // no deposit limitation is placed for this token

Then, for this token, the administrators decide to set depositLimitation = true again. The user calls L1ERC20Bridge.deposit() and again deposits an amount close to the depositCap. But it can no longer perform the function, because
require(totalDepositedAmountPerUser[_l1Token][_depositor] + _amount <= limitData.depositCap, "d1");

Although he already called the claimFailedDeposit() function. This causes the L1ERC20Bridge.sol contract to not function correctly.

## Tools Used
Manual review

## Recommended Mitigation Steps
Add reduction in totalDepositedAmountPerUser even if depositLimitation = false


## Assessed type

Context
Risk of freezing tokens in the L1ERC20Bridge and `claimFailedDeposit()` can have multiple calls 
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

# Vulnerability Report

There is a risk of freezing tokens in the `L1ERC20Bridge` contract if a deposit finalization fails on the L2 network. When a user initiates a deposit on the L1 network, the deposited funds are locked in the `L1ERC20Bridge` contract until the deposit is successfully finalized on the L2 network. If the deposit finalization fails on the L2 network, the deposited funds remain locked in the L1ERC20Bridge contract and cannot be withdrawn by the user.

While the `L1ERC20Bridge` contract provides a `claimFailedDeposit()` function that allows users to claim their deposited funds back to their L1 address in case of a failed deposit finalization. The function requires the user to provide proof of the failed deposit finalization on the L2 network, and it checks the depositAmount mapping to determine the amount of funds that were deposited by the user for the specified L1 token address and L2 deposit transaction hash. If the deposited funds are greater than zero, the function transfers the funds back to the user's L1 address.

However, it is important to note that the `claimFailedDeposit()` function is not a guaranteed solution to the risk of freezing tokens. If the L2 network is experiencing **high congestion** or other issues, it may not be possible to finalize deposits in a timely manner, which could result in funds being locked in the `L1ERC20Bridge` contract for an extended period of time. Making it a warnning for users to carefully consider the risks and benefits of using this for their depositing funds.

Also, if a large amount of funds are locked in the L1ERC20Bridge contract due to a failed deposit finalization, it could have a significant impact on the users who deposited those funds, as they would not be able to access their funds until the issue is resolved.

Also, i'm particullary concerned about `claimFailedDeposit()`, indeed the function is not vulnerable to reentrancy attacks, as it uses the `nonReentrant` modifier to prevent multiple calls to the function before the current call has completed, but I still recommend to move the transfer of funds to the user's L1 address to the **beginning of the function**, before any other operations are performed. This will ensure that the function cannot be called again before the funds have been transferred.

*****Let me be a bit more clear and try to make a report more readable as follows.

## Executive Summary

This report aims to analyze and address vulnerabilities in the zkSync network and its associated smart contracts. I have identified potential risks and provided recommendations to enhance the security and reliability of zkSync.

## Vulnerability Details

### Description

The zkSync network has specific vulnerabilities and risks:

1. **Risk of Freezing Tokens in L1ERC20Bridge**:
Contracts L1WethBridge.sol and L1ERC20Bridge.sol may not function correctly due to an incorrect deploy address of the l2Bridge contract
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L102
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L126
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/libraries/BridgeInitializationHelper.sol#L30-L58
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L102
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L126


# Vulnerability details

## Impact
An attacker can interfere with the operation of the L1WethBridge.sol and L1ERC20Bridge.sol functions by occupying a pre-calculated address

## Proof of Concept
The L1WethBridge.sol and L1ERC20Bridge.sol contracts have an initialize() function that initializes the contract bridge for further use.

To deploy contracts, the requestDeployTransaction() function is called. However, requestDeployTransaction() does not check the return value of the deployed contract address. To deploy a contract, use the create2 function, which, if unsuccessful, will return the value address(0). For example, this value can be obtained if an attacker sees data from a function call from the mempool and uses this address with his transaction.

Thus, address bridgeImplementationAddr can be equal to 0 (https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L102).

In the same way, address l2Bridge can be equal to 0 (https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L126).

Failure to check for address(0) when deploying a contract may result in incorrect behavior later on.

## Tools Used
Manual review

## Recommended Mitigation Steps
Add the bytes32 canonical TxHash hash from the zkSync.requestL2Transaction() function to the calculation to create address randomness


## Assessed type

Context
reentrancyGuardInitializer modifier used on constructor and Initialize functions
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L64
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1WethBridge.sol#L87
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L65
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L89
https://github.com/code-423n4/2023-10-zksync/blob/main/code/contracts/ethereum/contracts/common/ReentrancyGuard.sol#L41-L59


# Vulnerability details

## Impact
L1WethBridge.sol and L1ERC20Bridge.sol each have two places where the reentrancyGuardInitializer modifier is used. This modifier is found both in the constructor function and in the initialization function. This is a problem because this modifier can only be called successfully once and will fail if called again. Since the constructor function runs automatically when the contract is deployed, this means that it is not possible to call the initialization function afterwards.

## Proof of Concept
Inside the reentrancyGuardInitializer modifier, it calls the _initializeReentrancyGuard function. The value of _NOT_ENTERED is then stored in the storage location specified by the LOCK_FLAG_ADDRESS variable. It then requires that LOCK_FLAG_ADDRESS be zero the first time this function is called so that the function does not fail require(lockSlotOldValue == 0, "1B");.

Let's look at this using the L1WethBridge.sol contract as an example:
When L1WethBridge.sol is deployed, lockSlotOldValue will be set because the reentrancyGuardInitializer modifier is in its constructor. This will make it impossible to call an initialization function that has the same modifier to deploy the L2 bridge counterpart during initialization, making it impossible to initialize a contract bridge for later use or pass input parameters to the initialize() function for use on L2.

## Tools Used
Manual review

## Recommended Mitigation Steps
The reentrancyGuardInitializer modifier should be removed from the constructor function since it is already used in the initialization function.


## Assessed type

Access Control
Unnecessary Limitation on claimFailedDeposit Function
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary
The senderCanCallFunction modifier is designed to determine whether the caller can call a specific function on the target contract. While it is acceptable to restrict users from depositing or calling other functions, it is not appropriate to prevent users from claiming their failed deposits. A contract should be designed in a way that doesn't allow any trusted role to directly block user access to their own funds.
```solidity
    function claimFailedDeposit(
        address _depositSender,
        address _l1Token,
        bytes32 _l2TxHash,
        uint256 _l2BatchNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBatch,
        bytes32[] calldata _merkleProof
    ) external nonReentrant senderCanCallFunction(allowList) {
```
## Impact
In cases where a deposit fails, if a user cannot call this function, and only certain authorities can call it, the user will lose access to their funds. This results in a direct loss of funds for the user.
## Proof of Concept (POC)
A test function demonstrates this vulnerability:
```solidity
    function test_RemovePermission_claimFailedDeposit() public {
        vm.prank(owner);
        allowList.setAccessMode(target, IAllowList.AccessMode.Closed);
        vm.prank(randomSigner);
        bool canCall = allowList.canCall(randomSigner, target, functionSig);
        assertEq(canCall, false, "User couldn't call and claim failed deposit");
    }
```
## Tools Used
Manual review.
## Recommendations
It is recommended to remove the senderCanCallFunction modifier from the claimFailedDeposit function to allow users to access their failed deposits without restrictions:
```diff
    function claimFailedDeposit(
        address _depositSender,
        address _l1Token,
        bytes32 _l2TxHash,
        uint256 _l2BatchNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBatch,
        bytes32[] calldata _merkleProof
-       ) external nonReentrant senderCanCallFunction(allowList) {
+       ) external nonReentrant {
```



## Assessed type

Access Control
Unauthorized Renounce Ownership in Governance Contract
Audit: 2023-10-zksync-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary
Two issues exist:
1. Functions related to the state of the Governance contract use the onlySelf modifier to enable proposed upgrades to modify two critical parameters: minDelay and securityCouncil. 
```solidity
    /// @notice Checks that the message sender is contract itself.
    modifier onlySelf() {
        require(msg.sender == address(this), "Only governance contract itself allowed to call this function");
        _;
    }
```
However, the renounceOwnership function lacks this modifier, allowing the owner to renounce ownership without a proposed upgrade and without waiting for the minDelay period.
2. The Governance contract inherits from the Ownable2Step contract but fails to remove the owner's ability to renounce ownership. As a result, the owner retains the capability to renounce ownership.
## Impact
This issue enables the owner to Renounce Ownership without going through the proposed upgrade process and without waiting for the minDelay period.
Given that the owner is the only one who can propose upgrades, if the owner renounces ownership, the contract will lose the ability to propose any further upgrades. 
## POC
A test function demonstrates this vulnerability:
```solidity
    function test_RenounceOwnership() public {
        vm.prank(owner);
        governance.renounceOwnership();
        assertEq(governance.owner(), address(0));
    }
```

## Tools Used
Manual review.
## Recommendations
It is recommended that the owner should not be able to renounce ownership. This functionality can be disabled by adding the following code:
```solidtiy
function renounceOwnership() public override onlyOwner {
	revert("Cannot renounce ownership");
}
```
If it is not feasible to disable this function, it should be called as part of a proposed upgrade, and therefore, it should include the onlySelf modifier:
```solidtiy
function renounceOwnership() public override onlySelf {
_transferOwnership(address(0));
```


## Assessed type

Access Control