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: 3/64
Findings: 8
Award: $140,480.79
π Selected for report: 3
π Solo Findings: 3
π Selected for report: Koolex
Also found by: Audittens, rvierdiiev
6776.3633 USDC - $6,776.36
https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L1667-L1669 https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L1681 https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/bootloader/bootloader.yul#L991-L996
The issue arises from the following sequence in the bootloader's behavior:
ZKSYNC_NEAR_CALL_executeL1Tx
function is responsible for executing an L1->L2 transaction.success = 0
), the nearCallPanic()
function is invoked.nearCallPanic()
function, when called, exhausts all the remaining gas for the current frame.getExecuteL1TxAndGetRefund
function, potentialRefund
is equal to zero.Modify the getExecuteL1TxAndGetRefund
function. First, calculate and store the value of gasSpentOnExecution
in memory. Only after storing this value should the nearCallPanic()
function be invoked. By doing this, even if nearCallPanic()
exhausts all the remaining gas, the gasSpentOnExecution
stored in memory can be used to correctly compute the potentialRefund
, ensuring compatibility with Ethereum's behavior.
Other
#0 - c4-pre-sort
2023-10-31T07:08:15Z
bytes032 marked the issue as low quality report
#1 - miladpiri
2023-11-08T14:15:23Z
True, but the operator can provide the additional refund. This is duplicate. Low/Medium.
#2 - c4-sponsor
2023-11-08T14:15:55Z
miladpiri (sponsor) confirmed
#3 - c4-judge
2023-11-28T12:55:19Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#4 - GalloDaSballo
2023-11-28T12:56:14Z
I believe I have judged similar findings, ultimately L1->L2 is not equivalent to any specific L1 TX and in case of a revert, the gas is consumed
I believe this can be argued in many ways
And one way is that this prevents spam of the L2
Downgrading to QA but open to hearing about higher impacts
#5 - Barichek
2023-12-04T12:11:52Z
@GalloDaSballo @vladbochok
this prevents spam of the L2
The argument that retaining the entire gas limit on failed transactions prevents spam on L2 is not technically substantiated. Firstly, spam prevention in blockchain networks primarily relies on the cost incurred for computational resources actually used, not the total gas limit set. In a scenario where a transaction reverts, only the gas corresponding to computational resources consumed (gas_spent
) should be charged. The remaining gas (difference between gas_limit
and gas_spent
) plays no role in spam prevention, as it represents unused resources.
Secondly, if an actor intends to spam the L2 network, they can simply set a lower gas limit close to the expected consumption, thereby minimizing their cost while still overloading the network. The current mechanism does not deter such behavior. Instead, it primarily impacts unaware users who set higher gas limits, expecting standard Ethereum-like refunds on failed transactions. This leads to unnecessary financial penalties without providing additional protection against spam.
Therefore, the retention of the full gas limit in the case of failed L1 to L2 transactions does not effectively contribute to spam prevention on the L2 network and could be reconsidered for a more balanced approach.
#6 - c4-judge
2023-12-10T18:37:19Z
This previously downgraded issue has been upgraded by GalloDaSballo
#7 - c4-judge
2023-12-10T18:37:19Z
This previously downgraded issue has been upgraded by GalloDaSballo
#8 - c4-judge
2023-12-10T18:37:33Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#9 - c4-judge
2023-12-10T18:39:17Z
This previously downgraded issue has been upgraded by GalloDaSballo
#10 - c4-judge
2023-12-10T18:39:17Z
This previously downgraded issue has been upgraded by GalloDaSballo
#11 - c4-judge
2023-12-10T18:39:55Z
GalloDaSballo marked the issue as duplicate of #979
#12 - c4-judge
2023-12-10T18:40:04Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: zero-idea
Also found by: 0x1337, 0xTheC0der, 0xstochasticparrot, Audittens, HE1M, Jeiwan, erebus, gumgumzum, leviticus1129, lsaudit, quarkslab, rvierdiiev
656.3255 USDC - $656.33
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/Constants.sol#L35 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/AccountCodeStorage.sol#L93-L95 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/AccountCodeStorage.sol#L131-L137
CURRENT_MAX_PRECOMPILE_ADDRESS
is set to uint256(uint160(SHA256_SYSTEM_CONTRACT))
which is address(0x02)
, while the real maximal used precompile address is ECMUL_SYSTEM_CONTRACT = address(0x07)
.
The AccountCodeStorage::getCodeHash
and AccountCodeStorage::getCodeSize
functions rely on this constant to emulate the Ethereum behavior of the corresponding opcodes.
The wrong value is returned in the AccountCodeStorage::getCodeHash
and AccountCodeStorage::getCodeSize
functions for the precompiles that were added with addresses greater than address(0x02)
.
Set the CURRENT_MAX_PRECOMPILE_ADDRESS
constant to be equal to ECMUL_SYSTEM_CONTRACT
or rename the mentioned constant to be MAXIMAL_POSSIBLE_PRECOMPILE_ADDRESS
(and set it to the upper bound for the possible precompiles addresses).
Other
#0 - c4-pre-sort
2023-10-31T11:36:37Z
bytes032 marked the issue as duplicate of #142
#1 - c4-pre-sort
2023-10-31T11:36:44Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-11-23T19:31:00Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: Audittens
32626.9346 USDC - $32,626.93
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1244-L1245 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1255-L1259 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1365 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1542 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/scripts/process.ts#L123 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/Compressor.sol#L54-L82
There is enforcement in the bootloader::sendCompressedBytecode
function that the data to be used for the call of the Compressor::publishCompressedBytecode
function is encoded in a standard way.
But this check contains bugs:
originalBytecodeOffset
is equal to afterSelectorPtr + 64
. This leads to the possibility of passing a great number of unused bytes as calldata of the call of the Compressor::publishCompressedBytecode
function. In this case, the calldata will have the following format:4 bytes : `publishCompressedBytecode` selector 32 bytes : offset for `_bytecode` parameter = V 32 bytes : offset for `_rawCompressedData` parameter = V + 32 + rounded_len(_bytecode) (V - 64) bytes : any bytes that will be ignored in the `publishCompressedBytecode` function 32 bytes : length of `_bytecode` parameter = len(_bytecode) rounded_len(_bytecode) bytes : `_bytecode` parameter = _bytecode 32 bytes : length of `_rawCompressedData` parameter = len(_rawCompressedData) rounded_len(_rawCompressedData) bytes : `_rawCompressedData` parameter = _rawCompressedData
safe
analogs should be used. This leads to the possibility of passing absolutely wrong data so the call of the Compressor::publishCompressedBytecode
function will revert. As this bug does not provide any strong attack vector I will present only the idea of how to achieve this, not the whole calldata: the offset of the _bytecode
parameter should be equal (2**256) - 64
(so the originalBytecodeOffset
will pass the checkOffset
check), rest of the parameters is easy to determine over this. The same happens in the bootloader::validateBytes
function, as in the bootloader::lengthRoundedByWords
function the overflow happens, leading to the same outcome.The ability of the operator to pass up to ~COMPRESSED_BYTECODES_SLOTS * 32 = ~1048576
additional bytes into the call of the Compressor::publishCompressedBytecode
function. Such possibility of spending part of the transaction gas limit makes the operator able to actually decrease the L2 transaction gas limit, which leads to the possibility of manipulation of the amount of gas used for the transaction execution.
The ability of the operator to pass encoding-related checks of the bootloader::sendCompressedBytecode
function, which leads to the possibility of forcing the Compressor::publishCompressedBytecode
function call to revert.
bootloader::sendCompressedBytecode
function:if iszero(eq(add(afterSelectorPtr, 64), originalBytecodeOffset)) { assertionError("Compression calldata incorrect") }
safeAdd
instead of add
in the corresponding calculations in the bootloader::sendCompressedBytecode
function.Other
#0 - c4-pre-sort
2023-11-01T08:02:31Z
bytes032 marked the issue as low quality report
#1 - c4-judge
2023-11-26T09:05:53Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#2 - Barichek
2023-12-04T11:08:24Z
@GalloDaSballo @vladbochok
I believe that the described finding is a medium-severity issue.
There is another issue which leads to possibility of [operator to burn excess gas as a strategy to maximize profits] and which is fairly evaluated as medium-severity. Current issue (especially, point "1)"
of the current issue) describes the same consequence, so it should be evaluated as the same severity.
Also, one of the consequences of this vulnerability is the possibility to [break trust expectations by purposefully making some txs to fail via OOG], while similar situation was judged as medium-severity. Moreover, this vulnerability opens for the operator ability to burn only part of the transaction gas, making it possible to manipulate on the amount of gas used for transaction execution.
#3 - c4-judge
2023-12-07T10:22:09Z
This previously downgraded issue has been upgraded by GalloDaSballo
#4 - c4-judge
2023-12-07T10:22:09Z
This previously downgraded issue has been upgraded by GalloDaSballo
#5 - GalloDaSballo
2023-12-07T10:23:18Z
The Warden has demonstrated how unsafe arithmetic could be used by the operator to pass incorrect data that "checks" down to the expected values
This could be used to pass incorrect compressed data as well as abuse gas costs as means to cause end users to be overcharged
Because of this, I agree with Medium Severity
#6 - c4-judge
2023-12-07T10:23:23Z
GalloDaSballo marked the issue as selected for report
#7 - c4-sponsor
2024-02-26T14:49:51Z
vladbochok (sponsor) confirmed
π Selected for report: Audittens
32626.9346 USDC - $32,626.93
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1244-L1245 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1255-L1259 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1365 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1562-L1579 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/Compressor.sol#L54-L82
In the bootloader::sendCompressedBytecode
function the nearCallPanic()
is used for the case of unsuccessfull call of the Compressor::publishCompressedBytecode
function. In practice, such situation could happen because of invalid data provided by the operator, as any of the internal checks of this function could be ignored by the operator to make the call to revert: decoding of the _rawCompressedData
(link), the checks on dictionary
length (link1, link2), the check on the encodedData
length (link), etc.
Such possibility of forcing the overall bootloader::ZKSYNC_NEAR_CALL_markFactoryDepsL2
to near call revert, makes the operator able to burn all the gas of the L2 transaction if there is at least one bytecode to be published at the start of transaction processing.
The ability of the operator to burn all the gas of the L2 transaction in the call of the bootloader::ZKSYNC_NEAR_CALL_markFactoryDepsL2
function, if at least one bytecode is to be published.
Use revertWithReason
instead of nearCallPanic
for the cases of unsuccessful calls of the Compressor::publishCompressedBytecode
function. The cases of out of gas error
on such calls operator can process as standard bytecode publications through the bootloader::markFactoryDepsForTx
functionality.
Other
#0 - c4-pre-sort
2023-11-02T14:02:58Z
141345 marked the issue as sufficient quality report
#1 - miladpiri
2023-11-06T17:54:04Z
This has medium severity because it gives the operator the ability to forcefully fail the transaction.
#2 - c4-sponsor
2023-11-06T17:54:12Z
miladpiri (sponsor) confirmed
#3 - c4-judge
2023-11-28T12:51:15Z
GalloDaSballo marked the issue as selected for report
#4 - GalloDaSballo
2023-11-28T12:51:53Z
The finding is similar to #71, however in this case it shows how the Operator can break trust expectations by purposefully making tx fails via OOG
π Selected for report: Audittens
32626.9346 USDC - $32,626.93
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L318 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L305-L306 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/zksync/facets/Mailbox.sol#L323 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1656-L1659 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L2321 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L2328-L2333 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/DefaultAccount.sol#L122-L129
ETH on L2 stored on EOAs is inaccessible through L1->L2 transactions, as for such transactions msg.value
is generated only "from L1 ETH", not from the active balance of the tx.origin
on L2. This is so because of the logic in the Malibox::_requestL2Transaction
function, which enforces msg.value
of the L1->L2 transaction to be only part of ETH to be minted inside of transaction processing on L2. Bootloader maintains this system invariant as well.
DefaultAccount
incorrectly assumes that L1->L2 transactions initiated by EOA cover the needed functionality of the executeTransactionFromOutside
function, so it is empty.
Users will be unable to access their ETH on L2 in the case of a malicious operator that ignores L2 transactions (so if it processes only L1->L2 transactions). Also, and importantly, this violates the security invariant that enforces the ability of the users to access L2 through L1->L2 transactions, and, therefore, withdraw funds from the rollup before the upgrade in case of a scheduled malicious upgrade.
No access to ETH on L2 that is controlled by EOAs in case of malicious operator.
Violation of the "guarantee of withdraw of funds before malicious upgrade execution" security invariant.
Add a possibility to create L1->L2 transactions that can use ETH from the L2 balance as part of the msg.value
. As an alternative, you can implement the DefaultAccount::executeTransactionFromOutside
function so it will contain the expected behavior, as stated in the comment above its declaration.
Other
#0 - c4-pre-sort
2023-10-31T09:39:17Z
bytes032 marked the issue as sufficient quality report
#1 - bytes032
2023-11-01T16:08:32Z
malicious operator ignores L2 transaction
#2 - miladpiri
2023-11-06T11:28:45Z
Warden states that if an EOA has some fund on L2, he can not withdraw it by requesting a tx from L1. The only way is to make a tx on L2. But, if the oprator is malicious and only process L1toL2 txs, not L2 txs, then userβs fund will be trapped.
The scenario requires a malicious opearot ignoring L2 txs. The main issue is that it is not possible to withdraw the fund of an account on L2 through L1 -> L2 transaction. This is duplicate of https://github.com/code-423n4/2023-10-zksync-findings/issues/827
Medium is fair.
#3 - c4-sponsor
2023-11-06T11:28:52Z
miladpiri marked the issue as disagree with severity
#4 - c4-sponsor
2023-11-06T11:49:16Z
miladpiri (sponsor) confirmed
#5 - c4-judge
2023-11-28T12:50:11Z
GalloDaSballo changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-11-28T14:47:16Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#7 - GalloDaSballo
2023-11-29T07:21:56Z
It seems like I may have misunderstood this findings and similar findings related to L1->L2 execution, please send me an explanation as to exactly what function is missing that prevents this from happening and I'll re-doup this category of issues
#8 - Barichek
2023-12-04T11:09:05Z
@GalloDaSballo @vladbochok
The main point of the described finding is about the violation of one of the main zkSync rollup invariants. Namely, the invariant mentioned in the finding is the "ability of the users to access L2 through L1->L2 transactions, and, therefore, withdraw funds from the rollup before the upgrade in case of a scheduled malicious upgrade".
This invariant should be maintained in the system design, as L1->L2 txs are currently the only one legal way to enforce the ability of the user to access his funds in case of the malicious operator. Therefore, it is a critical point that is the only difference between the current zkSync design and the sidechain with transitions proved by zkp.
Otherwise, it wouldn't make sense to publish state diffs using on-chain calldata: such data will be required to reconstruct the rollup state and force/process transactions in case of the malicious operator, but in case the operator can ignore L2 transactions and there is no access to ETH on L2 through L1->L2 txs there is no protection against the malicious operator. This confirms that the described vulnerability is no less serious than the "ability of the operator to not publish some of the rollup state diffs" or the "ability of the operator to ignore L1->L2 transactions (in the long-term system design with escape hatch)" (both of which are high-severity findings).
While the finding #48 and its duplicates are about the incorrect logic of the refundRecipient
aliasing process, this finding is about violation of one of main system invariant.
Along with the facts that most of TVL of zkSync is native ETH (link1, link2), and custom wallets are not widely adopted on zkSync, most of the value locked in the zkSync fit the described situation.
The described above confirms the fact that the described vulnerability is a high-severity issue.
#9 - vladbochok
2023-12-04T13:04:06Z
Hey @GalloDaSballo @Barichek,
I agree that #48 is a different finding, that describes another issue. Also, I can confirm that this is a valid submission. That being said by @Barichek, the problem is censorship resistance protection, so user can't escape funds from the rollup using L1 -> L2 communication.
#10 - 0xunforgiven
2023-12-04T13:31:10Z
Hey @GalloDaSballo, @vladbochok. Thanks for reviewing my comment.
In following the above comment from @vladbochok regarding that #803 is different issue:
I just want to add that Issue #457 explain the both impacts of the issue root cause for EOA and L1-contracts:
there could be a lot of scenarios. the fact that L1-contracts can't spend and send its own balance in L2 (the balance in aliased address) is a critical bug. This bug would happen for EOA addresses to but the impact is that EOA addresses can't handle their funds from L1 but they can still create L2 transaction directly. so the impact would be that the censorship mechanism in zkSync L1 contracts (queue of L1->L2 transaction) won't work for all transactions. and users for handling their L2 balance need to create L2 transaction directly which can be censored.
In case the issues #803 (EOA impact) and #827 (L1-contracts impact) get validated as separate issues, I believe #457 should be duplicate of both of them.
I also put more comments about this issue's root cause and impacts(which are explained in #457) in comment1 and comment2
#11 - c4-judge
2023-12-10T18:18:54Z
This previously downgraded issue has been upgraded by GalloDaSballo
#12 - c4-judge
2023-12-10T18:18:54Z
This previously downgraded issue has been upgraded by GalloDaSballo
#13 - c4-judge
2023-12-10T18:19:06Z
GalloDaSballo marked the issue as selected for report
#14 - GalloDaSballo
2023-12-10T18:20:52Z
The Warden has shown how due to a lack of implementation of `executeTransactionFromOutside`Β a malicious operator could prevent any withdrawal by denying L2 txs
Because of the specific scoping this is valid, because of the reliance on a malicious operator, Medium Severity seems the most appropriate
#15 - Audittens
2023-12-10T20:06:42Z
Hi @GalloDaSballo.
With all due respect, I believe that this issue should be classified as high-severity vulnerability.
Main possible impact of most of the already approved high-severity vulnerabilities is the possibility of the operator (and only of him) to maliciously steal funds:
A malicious validator could generate and submit a proof with incorrect behavior of the div instruction.
Attacker can manipulate the sorted queue in log sorter, constraints are not strong enough and reverted l1 logs and events can still be emitted.
Attempting to read a word that spans across the pointer bound start+offset should return zero bytes for the addresses. When offset >= length, the result should be completely zeros, so we can skip the read. However, in current implementation, this case is not handled properly so that attacker can forge arbitary read result.
A malicious validator could generate and submit a proof with incorrect behavior of the shr instruction.
Attacker can forge arbitary result for opcode and and or; as for xor, an overflowed UInt8 will stay in the circuit, which can lead to unexpected behavior.
While the above-mentioned reports lead to some sort of unexpected behaviours and incorrect results of execution of some of VM functionality, the current report (#803) gives the operator ability to completely freeze the ETH value stored on zkSync (with absolutely no way to prove on L1 that the operator is ignoring l2 txs).
I believe, that such possibility leads to practically the same consequences, as the ability of the operator to steal the funds -- it is actually the analogue of the game where one party (in our case it is an operator) have an unique right to completely freeze funds of the other party (in our case it is the users), along with the unique right to unfreeze the funds at any moment in the future. The optimal result of this game have (asymptotically) the same consequences as the result of the situation where the first party can easily steal the funds of the other.
Along with the mentioned facts that most of TVL of zkSync is native ETH (link1, link2) and custom wallets are not widely adopted on zkSync (so most of the value locked in the zkSync fit the described situation), I believe that this issue is a high-severity vulnerability.
#16 - xuwinnie
2023-12-10T23:49:52Z
@Audittens This issue relies on the assumption that operator is centralized, but in the foreseeable future, zksync operator will go decentralized. Before the decentralization, this issue slightly increases the trust assumption. It's worth noting that currently priority queue only ensures censorship resilience( operator can still completely ignore the queue, but can't select from the queue) So even if this issue is fixed, operator can still freeze everyone's fund.This issue only lets the operator to freeze certain user's fund without disrupting the priority queue. After the decentralization, the highs you mentioned will be exploitable, while this issue will no longer exist.
3293.3126 USDC - $3,293.31
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/governance/Governance.sol#L70-L76 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/governance/Governance.sol#L154-L158 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/contracts/ethereum/contracts/governance/Governance.sol#L186
According to the implementation of the Governance::cancel
function, the security council can cancel any operation that was scheduled by the governance owner.
While this feature creates the right for the security council to cancel malicious operations scheduled by the owner, it does not create additional protection for users of the rollup -- protection against a scheduled malicious operation (in practice, rather, a malicious upgrade) comes from the minimum delay for such operations, and only such protection can be considered as guaranteed by the smart contract and sufficient (in practice it is sufficient due to invariant that a user will be able to enforce the execution of an L1->L2 operation using L1 priority queue while the upgrade has not yet been completed).
At the same time, this feature creates unnecessary security assumptions about the security council.
As a demonstrative example of the problem (with a realistic application of the governance functionality -- rollup upgrades management), one can consider the following scenario:
In such a situation, the owner will schedule an upgrade to fix the vulnerability and protect the system. But there is a very powerful economic incentive to cancel such a scheduled upgrade (and in general all owner's attempts to upgrade the rollup).
Also, the system does not have any (at least publicly mentioned) incentive/enforcement that the security council will execute an instant upgrade in case of the analogical scenario but with a smaller required time delay for the attacker.
A very powerful economic incentive for the security council to cancel upgrades proposed by the owner, with almost zero practical benefit to the system from such a possibility.
Lack of system incentive/enforcement that the security council will execute instant upgrades in case of need.
Allow only the governance owner to cancel scheduled governance operations.
Create additional incentives for the security council to execute instant upgrades if necessary.
Access Control
#0 - c4-pre-sort
2023-10-31T09:45:57Z
bytes032 marked the issue as low quality report
#1 - miladpiri
2023-11-08T14:24:53Z
#2 - c4-judge
2023-11-23T16:47:28Z
GalloDaSballo marked the issue as duplicate of #260
#3 - c4-judge
2023-11-28T15:52:47Z
GalloDaSballo marked the issue as satisfactory
25097.642 USDC - $25,097.64
https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/contracts/DefaultAccount.sol#L222-L227 https://github.com/code-423n4/2023-10-zksync/blob/main/code/system-contracts/contracts/DefaultAccount.sol#L45
The DefaultAccount
contract's fallback
function is designed to prevent any calls from the bootloader using an assertion check against the bootloader's formal address. However, the fallback
function doesn't consider the scenario where another account, when invoked by the bootloader, uses a delegatecall
to call the DefaultAccount
contract's fallback
. In such a scenario, the assertion check will fail, causing the contract to panic instead of behaving neutrally.
Someone can create a contract that, when invoked by the bootloader, utilizes a delegatecall
to the DefaultAccount
's fallback
function. This will trigger the assert statement, causing the DefaultAccount
to panic, which is an unintended behavior.
The vulnerability may lead to unexpected behavior in the contract operation, potentially causing disruptions or, in worst-case scenarios, loss of funds or other unintended consequences.
Add ignoreInDelegateCall
modifier to DefaultAccount
's fallback
function.
call/delegatecall
#0 - c4-pre-sort
2023-10-31T07:06:58Z
bytes032 marked the issue as low quality report
#1 - miladpiri
2023-11-08T14:17:47Z
#2 - c4-judge
2023-11-21T13:04:42Z
GalloDaSballo marked the issue as duplicate of #168
#3 - c4-judge
2023-11-28T12:48:25Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: HE1M
Also found by: Audittens, OffsideLabs, zkrunner
6776.3633 USDC - $6,776.36
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1244-L1245 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1255-L1259 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1365 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/scripts/process.ts#L123 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/Compressor.sol#L54-L82 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/contracts/Compressor.sol#L62
There is no enforcement in the system that the compressed bytecode to be published was compressed optimally. Moreover, there is no enforcement that the compressed data to be published is not longer than the original bytecode. The only upper bounds for the unused data of "_rawCompressedData" (specifically, dictionary
) are 524288 bytes
from the Compressor::publishCompressedBytecode
function and ~32768 slots == ~1048576 bytes
from the bootloader memory restriction.
This gives the operator the ability to spend a great amount of gas on the emulation of the publication of a fake "compressed bytecode" (with length up to ~524288 bytes
) in any case, even when the real bytecode is relatively short. This can cost for the transaction initiator the same as publication of ~524288 non-zero bytes
in L1 calldata (which is ~524288 * 16 = ~8'388'608
gas on L1). Please note, that the operator actually will not publish this data on L1 in most of such cases, as in practice users will not initiate transactions with so a great L2 gas limit. And even for the cases when the gas limit of the transaction enforces publication of such data on L1, this attack is profitable for the operator.
Ability of the operator to perform publication of up to ~524288
bytes of the "compressed bytecode", for each of the bytecodes that are to be published at the transaction preparation.
In most of the practical cases, the gas for such operation will be burned (the cost of it will be paid by the user and the "body" of the L2 transaction will be not executed), while the real publication of the bytecode will be skipped due to revertion of the bootloader::ZKSYNC_NEAR_CALL_markFactoryDepsL2
function.
Also, and importantly, such possibility of spending part of the transaction gas limit makes the operator able to actually decrease the L2 transaction gas limit, which leads to the possibility of manipulation of the amount of gas used for the transaction execution.
Enforce that the length of the compressed data in the bytecode publication is not greater than the original bytecode length.
Namely, you can add the following check to the Compressor::publishCompressedBytecode
function:
require(_rawCompressedData.length < _bytecode.length, "Compressed data should be shorter than the original bytecode");
To make this check more accurate, there should be a counting of non-zero bytes in both arrays and the corresponding weighted check.
Other
#0 - c4-pre-sort
2023-11-01T08:01:43Z
bytes032 marked the issue as low quality report
#1 - miladpiri
2023-11-08T14:22:26Z
Low, as it is not clearly mentioning that how the operator can consume lots of gas, just mentioning that make a long compressed data.
#2 - c4-sponsor
2023-11-08T14:22:31Z
miladpiri marked the issue as disagree with severity
#3 - c4-judge
2023-11-16T13:50:53Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#4 - Barichek
2023-12-04T11:08:29Z
@GalloDaSballo @vladbochok
The described finding is an absolute duplicate of the finding #71.
The description in the "Description" section of the report states the possibility of the elongation of the dictionary
parameter with unnecessary data up to 524288
bytes.
#5 - c4-judge
2023-12-07T18:58:28Z
This previously downgraded issue has been upgraded by GalloDaSballo
#6 - c4-judge
2023-12-07T18:58:28Z
This previously downgraded issue has been upgraded by GalloDaSballo
#7 - c4-judge
2023-12-07T18:58:49Z
GalloDaSballo marked the issue as duplicate of #71
#8 - c4-judge
2023-12-07T18:58:54Z
GalloDaSballo marked the issue as satisfactory