This is a report of a finding in bootloader.yul. While the file is out of scope of the contest, the sponsor stated that they would still accept findings in the file and would judge them separately from the contest.
A bytecode hash for which the bytecode (preimage) wasn’t published to the L1 can be marked as known on the L2. This will break mining and proving of blocks because calls to contracts with invalid or missing bytecode cannot be proved. The issue cannot be resolved after deployment: the preimage (i.e. bytecode) of a butecode hash that has been marked as known cannot be re-published to the L1 since the bootloader doesn’t allow publishing known bytecodes to avoid overpaying.
As per the description of the project:
> On zkSync, the L2 stores the contract’s code hashes and not the codes themselves. Therefore, it must be part of the protocol to ensure that no contract with unknown bytecode (i.e. hash with an unknown preimage) is ever deployed.
Also:
> If the transaction comes from L2, i.e. (the factory dependencies are yet to publish on L1), the operator prepares the compress the bytecode offchain and then verifies that the bytecode was compressed correctly.
And also:
> A call to a contract with invalid bytecode can not be proven. That is why it is essential that no contract with invalid bytecode is ever deployed on zkSync.
In accordance with this description, in the beginning of transaction execution, the bootloader marks bytecode hashes provided by the operator as known. It also validates and sends compressed bytecode to L1. This is done in the ZKSYNC_NEAR_CALL_markFactoryDepsL2 function:
This aligns with the description of how new contract bytecodes are marked as known and sent to L1 to guarantee that every bytecode hash on L2 has a valid bytecode published on L1.
Now, let’s return to ZKSYNC_NEAR_CALL_markFactoryDepsL2 and see what happens after compressed bytecodes were processed:
> // For all the bytecodes that have not been compressed on purpose or due to the inefficiency
// of compressing the entire preimage of the bytecode will be published.
Thus, we expect that the call to markFactoryDepsForTx will publish all uncompressed bytecodes to L1.
SystemContractHelper.toL1(true, _bytecodeHash, _l1PreimageHash);
This results in a flaw in the bytecodes handling logic: if a bytecode is not compressed and not provided by the operator (i.e. it’s not handled by the sendCompressedBytecode function), it won’be sent to L1–only its hash will be sent (KnownCodesStorage.markFactoryDeps sends only bytecode hashes, not bytecodes). Yet it’ll still be marked as known on L2. The latter means that, if the operator doesn’t provide the bytecode of a contract, it cannot be provided afterwards because the bytecode hash will be known and the bootloader will revert on this line, while trying to publish an already known bytecode. There doesn’t seem to be a resolution to this situation, since bytecodes can only be published via the sendCompressedBytecode function. This will break the requirement that all bytecode deployed on the L2 must be published on the L1:
> On zkSync, the L2 stores the contract’s code hashes and not the codes themselves. Therefore, it must be part of the protocol to ensure that no contract with unknown bytecode (i.e. hash with an unknown preimage) is ever deployed.
And it will also break the prover, since a call to a contract with unknown bytecode cannot be proved.
It’s also worth noting that, while the operator is trusted to provide compressed bytecode that’s cheaper to send to L1 than sending the original bytecode, the specification doesn’t mention that it’s also trusted to provide all bytecodes and hashes:
> The operator is trusted to provide compressed bytecodes which are advantageous for the users, i.e. it is trusted to make sure that using compression is cheaper than simply publishing the original preimage.
Thus, the operator might not provide all bytecodes required by a transaction, either deliberately or due to a bug.
Manual review
It seems that the implementation of KnownCodesStorage.markFactoryDeps should publish full bytecodes to L1, since, besides being used to mark hashes as known, it’s also calls the _sendBytecodeToL1 function. This is also pointed at by the documentation of the _shouldSendToL1 argument of the internal _markBytecodeAsPublished function:
> /// @param _shouldSendToL1 Whether the bytecode should be sent on L1
And also the documentation of the _sendBytecodeToL1 function:
> /// @notice Method used for sending the bytecode (preimage for the bytecode hash) on L1.
However, the function only sends bytecode hashes, not bytecodes.
An alternative solutions seems requiring that the operator provides all compresses bytecodes for all factory dependencies of a transactions. This can be implemented as a revert when the bytecode hashes don’t match on this line.
To put it more generally, the ZKSYNC_NEAR_CALL_markFactoryDepsL2 function of the bootloader should mark a bytecode hash as known only if:
it’s not known;
a. its bytecode (preimage) is provided by the operator and matches the hash (this is not implemented currently);
b. or, its compressed bytecode is provide by the operator and is verified (this is implemented in the sendCompressedBytecode function of the bootloader);
The text was updated successfully, but these errors were encountered:
All reactions