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: 11/64
Findings: 2
Award: $12,177.85
š Selected for report: 0
š Solo Findings: 0
š Selected for report: OffsideLabs
11293.9389 USDC - $11,293.94
Malicious users or Operators can execute older, pre-EIP-155 transactions in the zkSync network. These transactions, originating from before Ethereum's mainnet (Spurious Dragon)[https://eips.ethereum.org/EIPS/eip-607] hardfork at block 2,675,000, are vulnerable to replay attacks, potentially leading to security risks such as drain of funds, griefing, and stealing (in rare cases).
The earliest Ethereum transactions, known as type 0 (legacy), lacked chain ID indication, rendering them vulnerable to replay attacks. If proper precautions aren't taken, these transactions can be executed on both L1 and L2 blockchains that employ the same transaction encoding as Ethereum's mainnet.
EIP-155, introducing crucial replay protection, was implemented in Ethereum's mainnet during the Spurious Dragon hardfork at block 2,675,000. Prior to this upgrade, all transactions remained unprotected.
The issue arises from the current implementation of the Bootloader and system contracts, inadvertently allowing the execution of legacy transactions on the zkSync network. As a result, potential attackers could search for transactions on the mainnet that belong to other users and then replay these transactions on zkSync.
Let's make an example, I'll show how an attacker can extract signature and make a valid transaction:
r
, s
, and v
values:
Request:curl -X POST \ -H "Content-Type: application/json" \ --data '{"jsonrpc": "2.0", "id": 1, "method": "eth_getTransactionByHash", "params": ["0x4a7c96b7ca00fe596653a5cf568716e1819f19390b39a88e3d57eadbd68f7cd8"]}' \ "https://eth.llamarpc.com"
Result:
{ "jsonrpc": "2.0", "id": 1, "result": { "blockHash": "0x1ed36094f3f8a89663a86d91b39396af173954d1df7b3292b3c6f652fc1e3441", "blockNumber": "0x1e8482", "from": "0x4b8a25120682dc5a5696d21c6d65f98442c7752f", "gas": "0x61a80", "gasPrice": "0x4a817c800", "hash": "0x4a7c96b7ca00fe596653a5cf568716e1819f19390b39a88e3d57eadbd68f7cd8", "input": "0x", "nonce": "0x0", "to": "0xb4bfefc30a60b87380e377f8b96cc3b2e65a8f64", "transactionIndex": "0x3", "value": "0xdc44abe8130000", "type": "0x0", "v": "0x1c", "r": "0x5d8eb834aabb531f7bb36543693f22f5ec7371ef8fca10cacea8cf62a763369a", "s": "0x1ed30a66f713290434fa5d5fd641cf5c450c4698c3eda8a04bd96f193cd9e709" } }
This retrieved data contains all the necessary values required to create and send a transaction. If the address specified in the from
field has a balance on the zkSync network, the attacker can craft a transaction with these extracted values and execute it successfully.
JSON RPC on the mainnet and other networks does not allow these transactions to be executed and will return only replay-protected (EIP-155) transactions allowed over RPC
, which provides minimal protection (since users can still commit transactions without JSON RPC). I can't verify if this is the case with zkSync, as the RPC and Operator's software are outside the scope of this contest. But even with protection at the software level, there's still the possibility of execution by malicious Operators that can't be censured.
High severity seems appropriate if there's no protection in the operator's software. Medium severity if there are some restrictions on who can execute the attack (such as the role of the operator).
Optimism and Base (which is built on Optimism) blockchains block pre-EIP-155 transactions on the protocol level: https://stack.optimism.io/docs/releases/bedrock/differences/#pre-eip-155-support
Polygon and Loopring block it at least on the RPC level. No mentions in the docs.
Manual review, MyCrypto to decode transactions
Disable ability to broadcast transactions without chain id. Revert when v
of a legacy transaction is equal to 27 or 28.
Other
#0 - c4-pre-sort
2023-11-01T05:35:06Z
bytes032 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-01T05:35:16Z
bytes032 marked the issue as duplicate of #882
#2 - c4-judge
2023-11-23T19:22:44Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#3 - ustas-eth
2023-11-29T20:16:19Z
I've extensively researched this issue and will provide the results here. I think it'll have a great deal of value for the sponsors and may change their minds or the judge's opinion because I believe validation of chain id for legacy transactions is not optional for rollups if you want to protect user funds.
The point of the report is that there are active addresses on the zkSync that have older transactions that should not be replayable. Many of these addresses have large transactions with mining rewards (thousands of ETH) that can potentially be executed as the network grows. Many of the inactive addresses will use zkSync eventually, so there's a continuous activation of new possibilities for the attack.
Quote from Optimism (https://stack.optimism.io/docs/releases/bedrock/differences/#pre-eip-155-support):
Pre-EIP-155 transactions do not have a chain ID, which means a transaction on one Ethereum blockchain can be replayed on others. This is a security risk, so pre-EIP-155 transactions are not supported on OP Stack by default.
Quote from Geth (https://blog.ethereum.org/2021/03/03/geth-v1-10-0, see ChainID enforcement):
... certain Ethereum based networks have been known to go offline due to replay issues...
Most of the major networks don't support pre-EIP-155 for the reasons explained in the report. Some of them:
The only difference from Ethereum that zkSync has is the higher intrinsic costs (200-300k of gas units for a transaction to be executed), so the pool of vulnerable transactions narrows down to mostly contract calls.
To find potential victims, you can use Dune with the query below. It will output all the addresses that have transactions before the EIP-155 (493,695 in total). Unfortunately, Dune doesn't provide transaction signatures, so it's impossible to differentiate them after the hardfork. Nonetheless, many of these addresses remain active on other L2 blockchains.
with non_eip155_addresses as ( select "from" as address from ethereum.transactions where type = 'Legacy' and block_number <= 2675000 group by "from" ) select address from non_eip155_addresses -- limit 100
The query below provides more specific information on potential victims. It will display addresses that have pre-EIP-155 transactions on Ethereum, along with a balance on zkSync. The balances are not very accurate. Dune is still developing native tables with balances, but this should be enough for the demonstration.
with non_eip155_addresses as ( select "from" as address from ethereum.transactions where type = 'Legacy' and block_number <= 2675000 group by 1 ), eth_balance as ( select bytearray_ltrim(cast(_account as varbinary)) as address, max_by(output_0, call_block_number) / 1e18 as amount from zksync_era_zksync.L2EthToken_call_balanceOf where _account in (select bytearray_to_uint256(address) as address from non_eip155_addresses) group by _account ) select address, amount from eth_balance order by amount desc limit 100
For example, this address: https://etherscan.io/address/0x8b5b540eacc727c36c80237f28105c91c0aa39a3 https://explorer.zksync.io/address/0x8b5b540eacc727c36c80237f28105c91c0aa39a3
It could have been exploited four months ago, before the latest transaction with nonce 4, when it had a balance of 0.82 ETH. The vulnerable transaction deploys a smart contract and has the gas limit set to 422,620: https://etherscan.io/tx/0xa87ca42886139d0f6260729b66734f795fe180d8458818983e7b4c38128410f6
#4 - ustas-eth
2023-11-30T11:30:53Z
Check out this comment on my other issue before the final evaluation: https://github.com/code-423n4/2023-10-zksync-findings/issues/155#issuecomment-1833567168
I would also like to add a little context to the example. It took me 5 minutes to find it, and with proper tools (Dune will make an update with balances soon) and databases, it's possible to get a wider pool of victims (including the transactions that were made after the hardfork) and monitor the victims continuously not to miss the opportunity of exploit (like in the example).
#5 - c4-judge
2023-12-05T12:51:22Z
This previously downgraded issue has been upgraded by GalloDaSballo
#6 - c4-judge
2023-12-13T15:52:22Z
GalloDaSballo marked the issue as satisfactory
#7 - c4-judge
2024-01-11T15:36:45Z
GalloDaSballo changed the severity to 2 (Med Risk)
š Selected for report: erebus
Also found by: 0xTheC0der, Bauchibred, HE1M, Jorgect, Udsen, alexfilippov314, chaduke, evmboi32, hash, ladboy233, lsaudit, oakcobalt, rvierdiiev, ustas, wangxx2026, xuwinnie, zero-idea, zkrunner
883.9114 USDC - $883.91
The current implementation of Mailbox
doesn't validate the size of the uint256 _l2GasLimit
in the requestL2Transaction()
function. It's essential for this value to be less than or equal to uint32.max
later on, as the Bootloader
will revert if this condition is not met. Currently, there's no issue because another parameter, _l2GasPerPubdataByteLimit
, is fixed at 800. However, this constant is expected to change in the future, as stated in the documentation and by the development team.
If, in the future, _l2GasPerPubdataByteLimit
is allowed to be set to a value greater than 71635 without additional checks, it would allow someone to input an extremely high _l2GasLimit
(2^32 and more), causing the transaction to consistently fail when a validator tries to create a new batch. Since L1 transactions are forced to be executed, this will trigger a system-wide DoS scenario, disrupting the system until a fix is applied. It's crucial to address this issue to prevent such disruptions and ensure compatibility with the Bootloader
.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L251-L256
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L3075-L3078
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L256
The documentation underscores the importance of ensuring that neither governance nor the security council can exploit their privileges to bypass the established limitations. While the assumption is that these entities are acting in good faith, it is crucial to prevent any potential misuse of power.
Governance is subject to a timelock mechanism for function calls, which is a commendable measure. However, there is a notable concern related to the Governance.scheduleShadow()
function, which could be utilized discreetly to perform upgrades without providing a clear description of the changes. Additionally, the absence of a minimum limit on the Governance.updateDelay()
function could result in a situation where minDelay
is set to zero. This configuration effectively grants administrators the ability to create and execute transactions, including potentially malicious upgrades, instantaneously within the same block.
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/governance/Governance.sol#L135-L145
Furthermore, administrators possess the capability to modify the security council to include themselves via the Governance.updateSecurityCouncil()
function. This action has the potential to introduce conflicts of interest and raise questions about the integrity of the system's governance structure. Addressing these concerns is essential to uphold the principles of transparency and accountability within the system.
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/governance/Governance.sol#L247-L259
ergs
term (deprecated) is used instead of gas
in the comments and documentation.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/contracts/ethereum/contracts/zksync/libraries/TransactionValidator.sol#L159
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L1179
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L1231
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L1233
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L1263
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L1596
There's no ERC721 and ERC1155 support in Governance
. This can lead to future problems with proposals that include transfer of these tokens. Add onERC721Received
, onERC1155Received
, and onERC1155BatchReceived
to the Governance
contract. See the original OZ's implementation:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/fd81a96f01cc42ef1c9a5399364968d0e07e9e90/contracts/governance/TimelockController.sol#L390-L421
Unused import of the Ownable
library in the Base
facet.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/contracts/ethereum/contracts/zksync/facets/Base.sol#L9
updateSecurityCouncil()
doesn't implement two-step transfer of privileges, similar to AdminFacet
and OZ's Ownable2Step
.
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/governance/Governance.sol#L254-L259
The ValidatorTimelock
currently has a vulnerability that allows two different validators to bypass its protection. This means that the timelock, which is meant to prevent immediate batch execution in the event of a zero-day vulnerability or stolen private keys, is ineffective. This issue becomes critical in a future scenario where multiple validators can participate, as per the project's goals.
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L75-L91
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L111-L128
The core purpose of the ValidatorTimelock
is to impose a delay before batch execution. However, if an attacker has access to two validators (one potentially stolen, and the other registered by the attacker), they can call ValidatorTimelock.commitBatches()
with the first validator and ValidatorTimelock.executeBatches()
with the second. This allows the batch to be successfully included since the timelock restriction applies only within the context of a single validator.
For future use, a better approach could be to implement a global storage contract that contains all the committed batches. This would prevent batch execution, regardless of the specific validator calling ValidatorTimelock.executeBatches()
.
Considering the project's plans to not use multiple validators at the moment and to rework the code during decentralization, this issue may be classified as low-severity or at least informational. It worth to notice there's a possibility in the code to add multiple validators.
There is another concern regarding the presence of multiple validators in the system. It's the possibility to call ValidatorTimelock.revertBatches()
or ExecutorFacet.revertBatches()
without any restrictions (if the caller is a validator, of course). In a scenario where multiple validators exist, one of them could hinder the others from executing batches by front-running their .executeBatches()
with the attacker's .revertBatches()
.
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/zksync/facets/Executor.sol#L394-L414
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/contracts/ethereum/contracts/zksync/ValidatorTimelock.sol#L93-L98
This situation has several implications:
.commitBatches()
and .proveBatches
, whereas the attacker only requires a single (and small) transaction, which is cheaper. This kind of behavior can be considered a griefing attack. That will make validation of batches unprofitable for the victims.The comment states "return 0", but instead reverts the execution. https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L3204-L3208
Function Bootloader.lengthRoundedByWords()
can overflow and return 0 if len
> type(uint).max - 30
. It is not a problem right now, because the overflow is checked indirectly in other functions. But since Bootloader
is considered as upgradeable, this can lead to an issue in the future.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L539-L542
Misleading _SLOT
is used in the name of the function, but it refers to the bytes instead (slot 523260, bytes 16744320, it's the end of the actual transactionās descriptions). Other functions that refer to bytes use _BYTES
in the end.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L434
Abstract contract with logic treated like an interface. It's inside the interfaces
folder and has a confusing I*
name, which are conventionally used to mark interfaces, not abstract contracts.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/contracts/interfaces/ISystemContract.sol
Typo in Bootloader
. Some functions use innerTxDataOffst
instead of innerTxDataOffset
.
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L2023-L2025
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L2047-L2049
https://github.com/code-423n4/2023-10-zksync/blob/ebec640786cf3cb791fa77a856ad9d2a554dad3f/code/system-contracts/bootloader/bootloader.yul#L2196-L2201
Keccak256
precompile contract uses lt(bytesSize, sub(BLOCK_SIZE(), 1))
condition to optimize execution. However, this means that in the case when bytesSize
equals 135, it will take the more resource-intensive path, even though the result is the same (40 gas). The correct condition should be lt(bytesSize, BLOCK_SIZE())
. This correction ensures that the optimization behaves as intended and avoids the costly path when bytesSize
is 135.
https://github.com/code-423n4/2023-10-zksync/blob/0d7a0f6b7770778da83820bc5e338602ca9d9938/code/system-contracts/contracts/precompiles/Keccak256.yul#L70
CIRCUIT_VERSOBE
. The correct name is CIRCUIT_VERBOSE
.
https://github.com/code-423n4/2023-10-zksync/blob/9c4aa015c755f4599db2256299ba02af4d21070c/code/era-zkevm_circuits/src/config.rs#L2
https://github.com/code-423n4/2023-10-zksync/blob/9c4aa015c755f4599db2256299ba02af4d21070c/code/era-zkevm_circuits/src/config.rs#L5#0 - bytes032
2023-11-06T12:57:11Z
3 r 10 nc
1 r
2 nc
3 nc
4 nc
5 bot
6 bot
7 r
8 r
1 nc
2 nc
3 nc
4 nc
5 nc
6 nc
1 nc
#1 - ustas-eth
2023-11-29T19:17:17Z
1 is a duplicate of https://github.com/code-423n4/2023-10-zksync-findings/issues/501
2 is a duplicate of https://github.com/code-423n4/2023-10-zksync-findings/issues/260 I argue differently in this one but point out the same functions in code. My issue is very similar to this one: https://github.com/code-423n4/2023-10-zksync-findings/issues/210:
updateDelay
can be set to 0updateSecurityCouncil()
doesn't validate the address (can be set to the owner themself or 0)
The centralization (and thus the possibility of misuse in case of a private keys theft) was also mentioned in my Analysis: https://github.com/code-423n4/2023-10-zksync-findings/issues/576
The need for validation of updateSecurityCouncil()
was also mentioned in number 6 of this QA.4 is a duplicate of Ladboy's QA report number 3: https://github.com/code-423n4/2023-10-zksync-findings/issues/984. In that report, it is considered a recommendation; in mine, it is considered a non-critical.
7 is a duplicate of https://github.com/code-423n4/2023-10-zksync-findings/issues/670 and https://github.com/code-423n4/2023-10-zksync-findings/issues/192
#2 - nethoxa
2023-11-29T19:54:51Z
About your 2 regarding my submission, its point was that if either governor or the security council got compromised, the whole governance mechanism was broken, even when its purpose was to prevent exactly that. It's not about the misuse of their privileges to act untrustworthy, as it is what you talk about in your analysis and your QA like setting delay to 0 and bypass the timelock or doing shadow upgrades, which are gonna be used only to fix bugs in production. Those are different things. It's true that you say:
"Despite this arrangement, there remains a potential risk of organized manipulative actions or the theft of private keys from multiple members of these multisig wallets. As a result, the community must actively monitor these multisigs to promptly respond to any undesirable actions or events."
But you do not even mention anything about the flawed trust model, it's just a vague sentence regarding theft of keys. Moreover, the security council/delay being set to 0 at deployment can be easily checked via etherscan/cast and the community would notice that pretty fast, so it's not a real threat and via shadow upgrades is not possible, as security council can ask for the operation and check if its hash match the one that has been submitted by governance, so it is not possible to pass a "hidden payload" to trick the security council without them knowing.
#3 - c4-judge
2023-12-10T20:21:05Z
GalloDaSballo marked the issue as grade-c
#4 - c4-judge
2023-12-10T20:34:46Z
GalloDaSballo marked the issue as grade-b
#5 - GalloDaSballo
2023-12-10T20:37:20Z
After reviewing 2.
I don't believe it is a duplicate
This finding asserts that the owner could change the council, but 260 acknowledges that this has a delay and that the council would cancel that
Am keeping as is
#6 - c4-judge
2024-01-10T10:08:10Z
GalloDaSballo marked the issue as grade-a
#7 - GalloDaSballo
2024-01-10T10:08:30Z
Given the penalty on 155, am raising this report to A due to high quality submissions