xanoutas/solidity-security-findings huggingface dataset:
`getCanonicalL1TxHash()` may return the same hash for different transactions
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L692-L707


# Vulnerability details

## Impact

Function `getCanonicalL1TxHash()` returns a canonical hash of the L1->L2 transaction, that will be sent to L1 as a message to the L1 contract that a certain operation has been processed.

Since it accepts just a `txDataOffset`, two different transactions with the same offset will return the same hash.

## Proof of Concept

[File: bootloader.yul](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/bootloader/bootloader.yul#L692)
```
/// @dev Calculates the canonical hash of the L1->L2 transaction that will be
            /// sent to L1 as a message to the L1 contract that a certain operation has been processed.
            function getCanonicalL1TxHash(txDataOffset) -> ret {
                // Putting the correct value at the `txDataOffset` just in case, since 
                // the correctness of this value is not part of the system invariants.
                // Note, that the correct ABI encoding of the Transaction structure starts with 0x20
                mstore(txDataOffset, 32)

                let innerTxDataOffset := add(txDataOffset, 32)
                let dataLength := safeAdd(32, getDataLength(innerTxDataOffset), "qev")

                debugLog("HASH_OFFSET", innerTxDataOffset)
                debugLog("DATA_LENGTH", dataLength)

                ret := keccak256(txDataOffset, dataLength)
            }
```

Let's consider two different L1->L2 transactions with the same offset. Since for both transactions offset is the same, the parameter `txDataOffset` will be the same for function `getCanonicalL1TxHash()`. This leads to the conclusion, that `getCanonicalL1TxHash()` will return the same hash, for two different transactions, when their offsets are the same.


## Tools Used

Manual code review


## Recommended Mitigation Steps

Refactor `getCanonicalL1TxHash()` - so that it will contain additional parameter - `nonce`. This `nonce` should also be used for hash calculation. Adding additional parameter `nonce` will make sure that `getCanonicalL1TxHash()` will return different hashes even for the transactions with the same `txDataOffset`.



## Assessed type

Other
`TransactionHelper.sol` violates EIP-712
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/libraries/TransactionHelper.sol#L84-L87
https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/libraries/TransactionHelper.sol#L25-L71
https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/libraries/TransactionHelper.sol#L118-L136


# Vulnerability details

## Impact

According to EIP-712 spec, the typehash should contain all the fields defined in the struct.
The current implementation of `EIP712_TRANSACTION_TYPE_HASH`, however, misses the `signature` field.


## Proof of Concept

[File: TransactionHelper.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/libraries/TransactionHelper.sol#L84-L87)
```
bytes32 constant EIP712_TRANSACTION_TYPE_HASH =
        keccak256(
            "Transaction(uint256 txType,uint256 from,uint256 to,uint256 gasLimit,uint256 gasPerPubdataByteLimit,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,uint256 paymaster,uint256 nonce,uint256 value,bytes data,bytes32[] factoryDeps,bytes paymasterInput)"
        );
```

`EIP712_TRANSACTION_TYPE_HASH` is defined as above.


[File: TransactionHelper.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/libraries/TransactionHelper.sol#L25-L71)
```
struct Transaction {
    // The type of the transaction.
    uint256 txType;
    // The caller.
    uint256 from;
    // The callee.
    uint256 to;
    // The gasLimit to pass with the transaction.
    // It has the same meaning as Ethereum's gasLimit.
    uint256 gasLimit;
    // The maximum amount of gas the user is willing to pay for a byte of pubdata.
    uint256 gasPerPubdataByteLimit;
    // The maximum fee per gas that the user is willing to pay.
    // It is akin to EIP1559's maxFeePerGas.
    uint256 maxFeePerGas;
    // The maximum priority fee per gas that the user is willing to pay.
    // It is akin to EIP1559's maxPriorityFeePerGas.
    uint256 maxPriorityFeePerGas;
    // The transaction's paymaster. If there is no paymaster, it is equal to 0.
    uint256 paymaster;
    // The nonce of the transaction.
    uint256 nonce;
    // The value to pass with the transaction.
    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. In order 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;
    // T
It's not possible to upgrade chainId
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/state-transition/StateTransitionManager.sol#L202-L217
https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol#L147-L157


# Vulnerability details

## Impact

Function `StateTransitionManager._setChainIdUpgrade()` which is responsible for upgrading chainId, sets `VerifierParams` with the values which won't be accepted by `BaseZkSyncUpgrade._setVerifierParams()` function.
Because of that - upgrading chainId won't be possible.

## Proof of Concept

[File: StateTransitionManager.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/state-transition/StateTransitionManager.sol#L202-L217)
```
ProposedUpgrade memory proposedUpgrade = ProposedUpgrade({
            l2ProtocolUpgradeTx: l2ProtocolUpgradeTx,
            factoryDeps: bytesEmptyArray,
            bootloaderHash: bytes32(0),
            defaultAccountHash: bytes32(0),
            verifier: address(0),
            verifierParams: VerifierParams({
                recursionNodeLevelVkHash: bytes32(0),
                recursionLeafLevelVkHash: bytes32(0),
                recursionCircuitsSetVksHash: bytes32(0)
            }),
            l1ContractsUpgradeCalldata: new bytes(0),
            postUpgradeCalldata: new bytes(0),
            upgradeTimestamp: 0,
            newProtocolVersion: protocolVersion
        });
```

As demonstrated above, function `StateTransitionManager._setChainIdUpgrade()` sets `VerifierParams` to:

```
            verifierParams: VerifierParams({
                recursionNodeLevelVkHash: bytes32(0),
                recursionLeafLevelVkHash: bytes32(0),
                recursionCircuitsSetVksHash: bytes32(0)
```

However, those params won't be accepted by `BaseZkSyncUpgrade._setVerifierParams()` function.

[File: BaseZkSyncUpgrade.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol#L147-L157
)
```
		if (
            _newVerifierParams.recursionNodeLevelVkHash == bytes32(0) &&
            _newVerifierParams.recursionLeafLevelVkHash == bytes32(0) &&
            _newVerifierParams.recursionCircuitsSetVksHash == bytes32(0)
        ) {
            return;
        }

        VerifierParams memory oldVerifierParams = s.verifierParams;
        s.verifierParams = _newVerifierParams;
        emit NewVerifierParams(oldVerifierParams, _newVerifierParams);
```

As demonstrated above, when either of the parameter is `bytes32(0)` - function `BaseZkSyncUpgrade._setVerifierParams()` will return, instead of continuing executing and updating `VerifierParams`.

The flow can be summarized as below:

1. `StateTransitionManager._setChainIdUpgrade()` sets `recursionNodeLevelVkHa
Incorrect `_setVerifierParams()` implementation
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol#L147-L157


# Vulnerability details

## Impact

To properly upgrade `VerifierParams`, none of the parameters: `recursionNodeLevelVkHash`, `recursionLeafLevelVkHash`, `recursionCircuitsSetVksHash` can be empty (`bytes32(0)`).
However, the current implementation of `_setVerifierParams()` does not enforce this requirement. If at least one  of these parameters is non-zero - `_setVerifierParams()` will upgrade `VerifierParams`.

This basically means, that it's possible to upgrade `VerifierParams` even when some of the parameters are `bytes32(0)`. This should not be possible. The `VerifierParams` should be upgraded only when all provided parameters are non-empty.

This behavior is even confirmed in the previous version of the zkSync code:

[File: previous contest](https://github.com/code-423n4/2023-10-zksync/blob/26547a90d569559ef5c80ff8c523010134e84a71/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol#L127)
```
    function _setVerifierParams(VerifierParams calldata _newVerifierParams) private {
        if (
            _newVerifierParams.recursionNodeLevelVkHash == bytes32(0) ||
            _newVerifierParams.recursionLeafLevelVkHash == bytes32(0) ||
            _newVerifierParams.recursionCircuitsSetVksHash == bytes32(0)
        ) {
            return;
        }
```

During the previous contest, function `_setVerifierParams()` was using `||` operator, while the current implementation of `_setVerifierParams()` is using `&&` operator.

## Proof of Concept

[File: BaseZkSyncUpgrade.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/upgrades/BaseZkSyncUpgrade.sol#L147-L157
)
```
        if (
            _newVerifierParams.recursionNodeLevelVkHash == bytes32(0) &&
            _newVerifierParams.recursionLeafLevelVkHash == bytes32(0) &&
            _newVerifierParams.recursionCircuitsSetVksHash == bytes32(0)
        ) {
            return;
        }

        VerifierParams memory oldVerifierParams = s.verifierParams;
        s.verifierParams = _newVerifierParams;
        emit NewVerifierParams(oldVerifierParams, _newVerifierParams);
```

As demonstrated above, function uses AND operator (`&&`) instead of OR (`||`). This basically means, that if at least one parameter is not `bytes32(0)` - then above condition won't be fulfilled and function will continue its execution and set `VerifierParams`.

E.g., let's consider a scenario, where:

```
_newVerifierParams.recursionNodeLevelVkHash == bytes32(0)
_newVerifierParams.recursionLeafLevelVkHash == bytes32(0)
_newVerifierParams.recursionCircuitsSetVksHash == bytes32(11)
```

Even though above params are not correct (`recursionNodeLevelVkHash` and `recursionLeafLevelVkHash` are `bytes32(0)`), function won't `return` at lines 147-153, because `recursio
Incorrect calculation of `keccakGasCost`
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/L1Messenger.sol#L50-L51


# Vulnerability details

## Impact

The `keccakGasCost` is not correctly calculated. The `keccakGasCost` will be greater then expected.
This issue is related to user's fund loss - because user - in same scenarios - will pay more gas than expected.



## Proof of Concept

[File: L1Messenger.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/system-contracts/contracts/L1Messenger.sol#L50-L51)
```
    function keccakGasCost(uint256 _length) internal pure returns (uint256) {
        return KECCAK_ROUND_GAS_COST * (_length / KECCAK_ROUND_NUMBER_OF_BYTES + 1);
    }
```

* Let's consider a scenario where `_length == KECCAK_ROUND_NUMBER_OF_BYTES`.

`keccakGasCost(KECCAK_ROUND_NUMBER_OF_BYTES) = KECCAK_ROUND_GAS_COST * (KECCAK_ROUND_NUMBER_OF_BYTES / KECCAK_ROUND_NUMBER_OF_BYTES + 1) = KECCAK_ROUND_GAS_COST * 2`.
When the `_length` is `KECCAK_ROUND_NUMBER_OF_BYTES`, the cost of keccak should actually be `KECCAK_ROUND_GAS_COST`, however it's twice as big as it should be.

In other words, when `_length` is in a form of `N * KECCAK_ROUND_GAS_COST` (`_length` is divisible by `KECCAK_ROUND_GAS_COST`), the keccak cost has one additional `KECCAK_ROUND_GAS_COST`.

For `keccakGasCost(N * KECCAK_ROUND_NUMBER_OF_BYTES)`  the cost should be exactly: `N * KECCAK_ROUND_NUMBER_OF_BYTES`, however function `keccakGasCost()` returns:
`keccakGasCost(N * KECCAK_ROUND_NUMBER_OF_BYTES) = KECCAK_ROUND_GAS_COST * (N * KECCAK_ROUND_NUMBER_OF_BYTES / KECCAK_ROUND_NUMBER_OF_BYTES + 1) = KECCAK_ROUND_GAS_COST * ( N + 1) = N * KECCAK_ROUND_GAS_COST + KECCAK_ROUND_GAS_COST`. As we see, there's additional `KECCAK_ROUND_GAS_COST`.




## Tools Used

Manual code review


## Recommended Mitigation Steps

When  `_length == N * KECCAK_ROUND_NUMBER_OF_BYTES`, function `keccakGasCost()` should return `N * KECCAK_ROUND_NUMBER_OF_BYTES` instead of `N * KECCAK_ROUND_NUMBER_OF_BYTES + KECCAK_ROUND_NUMBER_OF_BYTES`


## Assessed type

Math
`depositAmount` is not properly updated in `L1ERC20Bridge.deposit()`
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L154


# Vulnerability details

## Impact

Function `L1ERC20Bridge.deposit()` initiates a deposit by locking funds on the contract and sending the request of processing an L2 transaction where tokens would be minted.
The amount of deposit is being assigned to `depositAmount[msg.sender][_l1Token][l2TxHash]` mapping.
However, calling `deposit()` does not increase `depositAmount[msg.sender][_l1Token][l2TxHash]`, but overwrites the old value of `depositAmount[msg.sender][_l1Token][l2TxHash]`.
This implies, that when user calls `deposit()` more than once - he will suffer for the fund loss, because the old value of `depositAmount[msg.sender][_l1Token][l2TxHash]` will be overwritten by the new value.


## Proof of Concept

[File: L1ERC20Bridge.sol](https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L154)
```
depositAmount[msg.sender][_l1Token][l2TxHash] = _amount;
```

As demonstrated above, `depositAmount[msg.sender][_l1Token][l2TxHash]` is not increased by `_amount`, but it is simply overwritten.

1. User calls `deposit()` with `_amount` set to 100.
2. User calls `deposit()` again, with `_amount` set to 50.
3. `depositAmount[msg.sender][_l1Token][l2TxHash]` should point to 150 (100 from the first call + 50 from the second call).
4. However, because of the `=` operator, `depositAmount[msg.sender][_l1Token][l2TxHash]` actually points to 50 (call from the 2. stage overwrote the `_amount` set during the 1. stage).
5. User suffers for loss, because the mapping `depositAmount[msg.sender][_l1Token][l2TxHash]` points that his deposit is only 50, while he deposited 150.

## Tools Used

Manual code review


## Recommended Mitigation Steps

Change:

```
depositAmount[msg.sender][_l1Token][l2TxHash] = _amount;
```

to:

```
depositAmount[msg.sender][_l1Token][l2TxHash] += _amount;
```


## Assessed type

Context
User might not be able to withdraw WETH/ETH on L1 with legacy l2Batchnumber, resulting in ETH permanently locked in L1SharedBridge.sol
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/bridge/L1SharedBridge.sol#L443-L447


# Vulnerability details

## Impact
A user might not be able to withdraw WETH/ETH on L1 with legacy l2Batchnumber, resulting in ETH  permanently locked in L1SharedBridge.sol. 

The new Mailbox is not always backward compatible.

## Proof of Concept
New Mailbox and L1SharedBridge are intended to be backward compatible. But the current implementation of Mailbox.sol and L1SharedBridge.sol' s withdrawal flow might cause certain legacy WETH deposits to be locked when withdrawn.

Suppose a user deposited WETH before the upgrade through old L1WETHBridge.sol. 
Now user initiated the withdrawal of WETH from old L2WETHBridge.sol before ERA upgrade.

(1) Since upgrade hasn't started or is about to start, user would still interact with the old L2WETHBridge.sol `withdraw()`, which burns user's L2Weth for ETH and send a ETH withdraw through `L2_ETH_ADDRESS.withdrawWithMessage{value: _amount}(l1Bridge, wethMessage);`. Note that this set `l1Bridge` as the l1 receiver (old L1WETHbridge.sol). The user designated `_l1Receiver` is part of `wethMessage`.
```solidity
//L2WethBridge.sol (legacy, for reference only)
    function withdraw(
        address _l1Receiver,
        address _l2Token,
        uint256 _amount
    ) external override {
...
        // WETH withdrawal message.
        bytes memory wethMessage = abi.encodePacked(_l1Receiver);

        // Withdraw ETH to L1 bridge.
        //@audit-info note: legacy L2WethBridge will put L1WethBridge address as l1Reciver. Actual user address(`_l1Receiver`) will be encoded in message.
|>       L2_ETH_ADDRESS.withdrawWithMessage{value: _amount}(l1Bridge, wethMessage);
```

(2) Now if L1 ERA upgrade hasn't started yet, the user will still call L1WethBrdige.sol's `finalizeWithdrawal()` to withdraw WETH. However, if L1 ERA-upgrade settles first before the user's `finalizeWithdrawal()` call. L1WethBridge.sol's `finalizeWithdrawal()` tx will call the new mailbox's `finalizeEthWithdrawal()` function. 

The problem occurs here: Although the new Mailbox.sol maintains `finalizeEthWithdrawal()`, this won't be backward compatible with a legacy WETH withdrawal flow. 

The new `finalizeEthWithdrawal()` will call L1SharedBridge's `finalizeWithdrawal()`. L1SharedBridge will send ETH to `l1Reciver`(L1WethBridge address, Not the user's address). However, the ETH transfer will revert due to L1WethBridge.sol will not recognize L1SharedBridge.sol's address and revert in the `receive()` fallback.
```solidity
//code/contracts/ethereum/contracts/state-transition/chain-deps/facets/Mailbox.sol
    function finalizeEthWithdrawal(
        uint256 _l2BatchNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBatch,
        bytes calldata _message,
        bytes32[] calldata _merkleProof
    ) external nonReentrant {
...
          //@audit-info note: in a le
User might be able to double withdraw during migration
Audit: 2024-03-zksync-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/bridge/L1SharedBridge.sol#L421-L427
https://github.com/code-423n4/2024-03-zksync/blob/4f0ba34f34a864c354c7e8c47643ed8f4a250e13/code/contracts/ethereum/contracts/bridge/L1SharedBridge.sol#L375


# Vulnerability details

## Impact
A user might be able to double withdraw during migration in some edge conditions: (1) if their withdrawal tx is included in a batch number the same or after `eraFirstPostUpgradeBatch`; (2)And if the user `finalizeWithdrawal` on the old L1ERC20Bridge.sol before L1ERC20Bridge.sol is upgraded. 

## Proof of Concept
The current migration process takes place in [steps](https://github.com/code-423n4/2024-03-zksync/blob/main/docs/Protocol%20Section/Migration%20process.md), which might allow edge conditions to occur. StepA: deploy new contracts(including L1SharedBridge); StepB:Era upgrade and L2 system contracts upgrade(at this point old L1ERC20Bridge still works); StepC: Upgrade L2 bridge and L1ERC20Brdige.sol. Then migrate funds to the new L1sharedBridge.

Since L1ERC20Bridge.sol is upgraded at the end. An edge condition can occur between StepA and StepB, where a user can still withdraw ERC20 tokens on the old L1ERC20Brdige.sol. 

Scenario: If a user `finalizeWithdrawal` ERC20 tokens on the old L1ERC20Bridge.sol first, they can withdraw again on L1SharedBridge.sol as long as the `_l2batchNumber` of the withdraw tx equals or is greater than `eraFirstPostUpgradeBatch`. In other words, `_isEraLegacyWithdrawal` check can be invalidated.

1. stepA: L1SharedBridge.sol will be deployed and initialized with a pre-determined value `eraFirstPostUpgradeBatch`;
2. UserA can initiate an ERC20 withdrawal tx on L2 bridge(currently old version);
3. stepB: Era upgrade is performed. UserA's withdrawal tx is included in the same `eraFirstPostUpgradeBatch` number;
4. UserA `finializeWithdrawal` on the old L1ERC20Brdige.sol.UserA should receive funds because funds haven't been migrated yet;
5. stepC: new L1ERC20Bridge.sol is upgraded and funds(ERC20) are migrated to L1SharedBridge.sol;
6. UserA `finializeWithdrawal` on L1SharedBridge.sol. This time, `_isEraLegacyWithdrawal(_chainId, _l2BatchNumber)` check will be bypassed,because user's `_l2BatchNumber` == `eraFirstPostUpgradeBatch`; UserA can receive the withdrawal funds a second time;
```solidity
//code/contracts/ethereum/contracts/bridge/L1SharedBridge.sol
    function finalizeWithdrawal(
        uint256 _chainId,
        uint256 _l2BatchNumber,
        uint256 _l2MessageIndex,
        uint16 _l2TxNumberInBatch,
        bytes calldata _message,
        bytes32[] calldata _merkleProof
    ) external override {
       //@audit-info when _l2BatchNumber>= eraFirstPostUpgradeBatch, `_isEraLegacyWithdrawal()` return false, checking on withdrawal status on legacyERC20bridge will be bypassed.
|>     if (_isEraLegacyWithdrawal(_chainId, _l2BatchNumber)) {