Platform: Code4rena
Start Date: 10/03/2023
Pot Size: $180,500 USDC
Total HM: 6
Participants: 19
Period: 9 days
Judge: GalloDaSballo
Total Solo HM: 2
Id: 221
League: ETH
Rank: 2/19
Findings: 4
Award: $27,100.66
π Selected for report: 2
π Solo Findings: 1
π Selected for report: unforgiven
Also found by: Franfran, HE1M, bin2chen, rvierdiiev
1968.2509 USDC - $1,968.25
Force deployment of a contract keep the flag of isConstructor
true, while in normal deployment, it is set to false at the end of the deployment.
When deploying a contract on zkSync, the flow is: createAccount/create2Account >> _nonSystemDeployOnAddress >> _performDeployOnAddress >> _storeConstructingByteCodeHashOnAddress _constructContract https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L281 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L289
In the _storeConstructingByteCodeHashOnAddress
, the flag of of isConstructor
will be set to true as well as the bytecodeHash
will be stored in AccountCodeStorage
.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L304
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/AccountCodeStorage.sol#L40
In the _constructContract
, the msg.value
will be transferred to the newly deployed contract, the constructor will be called, and finally the flag of isConstructor
will be set to false, indicating that the contract is not in constructing mode anymore (it is already constructed).
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L326
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/AccountCodeStorage.sol#L52
The inconsistency is that if a contract is deployed through force deployment, the flow is: forceDeployOnAddresses >> forceDeployOnAddress >> _storeConstructingByteCodeHashOnAddress _constructContract (it may not be called) https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L214 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L223
It is similar to normal contract deployment until storing bytecode hash on the address, but if _deployment.callConstructor
is false, the _constructContract
will not be invoked.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L222
It means that forcing deployment of a contract if _deployment.callConstructor
is false, will not mark the deployed contract as constructed (setting the flag isConstructor
to false). So, this contract will always be considered as in constructing mode.
In other words, unlike the normal deployment of contracts that at the end of deployment the flag isConstructor
was set to false, the force deployment will keep the flag as true.
This means that to get extcodehash
of this newly force-deployed contract, the result will be EMPTY_STRING_KECCAK
. This not true, it should return the getRawCodeHash
.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/AccountCodeStorage.sol#L92
Moreover, to get extcodesize
of this newly force-deployed contract, the result will be 0. This is not true, it should return the real length of the code.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/AccountCodeStorage.sol#L119
Even if during force deployment, the _deployment.callConstructor
is false, the function markAccountCodeHashAsConstructed
should be called, as recommended below:
function forceDeployOnAddress(ForceDeployment calldata _deployment, address _sender) external payable onlySelf { _ensureBytecodeIsKnown(_deployment.bytecodeHash); _storeConstructingByteCodeHashOnAddress(_deployment.newAddress, _deployment.bytecodeHash); AccountInfo memory newAccountInfo; newAccountInfo.supportedAAVersion = AccountAbstractionVersion.None; // Accounts have sequential nonces by default. newAccountInfo.nonceOrdering = AccountNonceOrdering.Sequential; _storeAccountInfo(_deployment.newAddress, newAccountInfo); if (_deployment.callConstructor) { _constructContract(_sender, _deployment.newAddress, _deployment.input, false); } else { ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.markAccountCodeHashAsConstructed(_deployment.newAddress); } emit ContractDeployed(_sender, _deployment.bytecodeHash, _deployment.newAddress); }
#0 - c4-judge
2023-03-24T09:15:29Z
GalloDaSballo marked the issue as primary issue
#1 - c4-judge
2023-03-24T09:16:07Z
GalloDaSballo marked the issue as duplicate of #171
#2 - c4-judge
2023-03-24T09:17:33Z
GalloDaSballo marked the issue as duplicate of #167
#3 - c4-judge
2023-04-05T12:02:05Z
GalloDaSballo marked the issue as satisfactory
π Selected for report: HE1M
Also found by: 0x73696d616f, minaminao, rvierdiiev
3553.7864 USDC - $3,553.79
Time-sensitive contracts will be impacted if deployed on zkSync.
Many contracts use block.number
to measure the time as the miners were able to manipulate the timestamp
(the timestamp
could be easily gamed over short intervals). So, it was assumed that block.number
is a safer and more accurate source of measuring time than timestamp
.
For instance, if a defi project sets 144000 block interval to release the interest, it means approximately 144000 * 12 = 20 days. Please note that each block in Ethereum takes almost 12 second.
If the same defi project is deployed on zkSync, it will not operate as expected. Because the there is no time-bound for the blocks in zkSync (the interval may be 30 seconds or 1 week). So, the time to release the interest can be between 50 days to 2762 days.
Since, it is assumed that zkSync is Ethereum compatible, any deployed contracts on Ethereum may deploy their contract in zkSync without noting such big difference.
Even if the contracts use timestamp
to measure the time, there will be another issue. In the contract SystemContext.sol
, it is possible to set new block with the same timestamp
as previous block, but with incremented block number.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/SystemContext.sol#L116
In other words, new blocks are created but their time is frozen. Please note that freezing time can not be lasted for a long time, because when committing block their timestamp
will be validated against a defined boundary.
It should be explicitly mentioned that block intervals in zkSync are not compatible with Ethereum. So, time-sensitive contracts will be noted.
Moreover, the equal sign should be removed in the following line: https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/SystemContext.sol#L116
#0 - c4-judge
2023-03-22T19:55:23Z
GalloDaSballo marked the issue as primary issue
#1 - miladpiri
2023-03-27T12:17:41Z
Two issues are stated:
The judges can decide better how to distinguish them.
#2 - c4-sponsor
2023-03-27T12:24:41Z
miladpiri marked the issue as sponsor confirmed
#3 - GalloDaSballo
2023-04-05T12:04:44Z
The warden has shown how the zkEVM could differ from the EVM in how block timing is enforced, because blocks can happen at inconsistent times, protocols contract could have their time assumptions broken.
Because this is a finding that has been addressed by other L2s, and causes inconsistent behaviour vs the EVM, I agree with Medium Severity
#4 - c4-judge
2023-04-05T12:04:49Z
GalloDaSballo marked the issue as selected for report
#5 - Minh-Trng
2023-04-08T00:40:06Z
Because this is a finding that has been addressed by other L2s, and causes inconsistent behaviour vs the EVM, I agree with Medium Severity
which L2s are those and how do they fix it? Intuitively I would have thought, that the discrepancy of blocktime between Ethereum and L2s is common sense and innate, because the whole thing of an L2 is to process multiple blocks and compress them before posting them into a single L1 block.
Have checked it only for arbitrum and optimism, but they have the same "issue": https://developer.arbitrum.io/time#block-numbers-arbitrum-vs-ethereum https://community.optimism.io/docs/developers/build/differences/#block-production-is-not-constant
#6 - GalloDaSballo
2023-04-10T08:26:43Z
Because this is a finding that has been addressed by other L2s, and causes inconsistent behaviour vs the EVM, I agree with Medium Severity
which L2s are those and how do they fix it? Intuitively I would have thought, that the discrepancy of blocktime between Ethereum and L2s is common sense and innate, because the whole thing of an L2 is to process multiple blocks and compress them before posting them into a single L1 block.
Have checked it only for arbitrum and optimism, but they have the same "issue": https://developer.arbitrum.io/time#block-numbers-arbitrum-vs-ethereum https://community.optimism.io/docs/developers/build/differences/#block-production-is-not-constant
You can see how Arbitrum does it in the link you sent and how protocols must adapt in order to use block values, for example, see GMX's fix: https://github.com/gmx-io/gmx-synthetics/blob/main/contracts/chain/Chain.sol
For OP you can see that they are working on a fix to offer consistent times: https://community.optimism.io/docs/developers/bedrock/differences/#the-evm
π Selected for report: HE1M
19499.514 USDC - $19,499.51
During force deployment, if the fund is not properly transferred to the to-be-force-deployed contract, the fund will remain in the contract ContractDeployer
and can not easily be recovered.
The function forceDeployOnAddresses
in contract ContractDeployer
is used only during an upgrade to set bytecodes on specific addresses.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L232
The ETH sent to this function will be used to initialize to-be-force-deployed contracts. The ETH sent should be equal to the aggregated value needed for each contract. https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L240
Then the function externally calls itself, and send the required value to itself. https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L245
If any of this call is unsuccessful, the whole transaction will not revert, and the loop continues to deploy all the contract on the provided newAddress
.
If for any reason, the deployment was not successful, the transferred ETH will remain in ContractDeployer
, and can not be used for the next deployments (because the aggregated amount is compared with msg.value
not the ETH balance of the contract). In other words, FORCE_DEPLOYER
fund will be in ContractDeployer
, and it can not be easily recoverred.
The possibility of unsuccessful deployment is not low:
It can happen if the bytecode is not known already. https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L213 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L296
It can happen during storing constructing bytecode hash. https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L214 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/AccountCodeStorage.sol#L36
It can happen during constructing contract and transferring the value. https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L223
By using try/catch, the fund can be transferred to an address that the governor has control to be used later.
function forceDeployOnAddresses(ForceDeployment[] calldata _deployments) external payable { // remaining of the code for (uint256 i = 0; i < deploymentsLength; ++i) { try this.forceDeployOnAddress{value: _deployments[i].value}( _deployments[i], msg.sender ) {} catch { ETH_TOKEN_SYSTEM_CONTRACT.transferFromTo( address(this), SomeAddress, _deployments[i].value ); } } }
#0 - GalloDaSballo
2023-03-24T09:27:53Z
Loss of funds due to reverts, keeping separate for now
#1 - GalloDaSballo
2023-03-24T09:28:16Z
See #95 for loss of value due to not calling constructor
#2 - miladpiri
2023-03-27T12:52:09Z
The goal was to by force deploy multiple of contracts (especially system contracts during the upgrade). If any deployment was unsuccessful, the whole transaction should not be reverted. So, the fund reverted during the failed deployment should be transferred to a valid address (not stay in ContractDeployer
) as suggested by the warden. The recommended mitigation is also good.
Severity is Medium.
#3 - c4-sponsor
2023-03-27T12:52:23Z
miladpiri marked the issue as sponsor confirmed
#4 - GalloDaSballo
2023-04-05T13:00:34Z
Per the Sponsors comment, the Warden has shown how, due to a lack of sweep on revert, funds sent in a sequence of multiple deployments can be lost when one of the deployment fails.
This is in contrast to having the entire deployment reverting
Because the behavior is unintended and funds can be lost conditionally on a revert, I believe Medium Severity to be appropriate
#5 - c4-judge
2023-04-05T13:00:39Z
GalloDaSballo marked the issue as selected for report
π Selected for report: unforgiven
Also found by: 0xSmartContract, Dravee, HE1M, Madalad, brgltd, bshramin, gjaldon, joestakey, minaminao, rbserver, rvierdiiev, supernova
2079.105 USDC - $2,079.11
In the document, it is noted that the gasLimit
is equal to 2^32-1
, but in the SystemContext
the value is hardcoded to 1<<30
.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/SystemContext.sol#L33
https://era.zksync.io/docs/dev/developer-guides/transactions/blocks.html#block-properties
The revert message is missing. https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/libraries/SystemContractHelper.sol#L152 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/L1Messenger.sol#L47 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/NonceHolder.sol#L135
The following check is not needed as there is casting to uint128 in line 60.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/MsgValueSimulator.sol#L48-L57
Using safeCastToU128
is recomended.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/libraries/Utils.sol#L19
The modifier onlySystemCall
is not needed as it is forced that msg.sender == DEPLOYER_SYSTEM_CONTRACT
.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/NonceHolder.sol#L134
msg.sender
is forced to be FORCE_DEPLOYER
contract address. So, no need to read msg.sender
.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L233
It can be hardcoded, like:
this.forceDeployOnAddress{value: _deployments[i].value}(_deployments[i], FORCE_DEPLOYER);
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L245
The same applies to line 212 as it only allows self call, so the _sender
is always FORCE_DEPLOYER
:
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L212
If by chance the _newAddress
during creating an account is in kernel space, the user can not deploy the contract.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L181
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/ContractDeployer.sol#L256
The probability is very very low, but since the kernel space is 2**16 -1
, the chance of address collision is higher than on Ethereum.
In that case, the user should change the bytecode of to-be-deployed contract, or increase his deployment nonce by deploying just a dummy contract.
If bootlaoder calls markFactoryDeps
, the parameter _l1PreimageHash
is set zero as the parameter sent to _markBytecodeAsPublished
.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/KnownCodesStorage.sol#L36
When BytecodeCompressor
during publishing the compressed bytecode, calls markBytecodeAsPublished
, parameter _shouldSendToL1
is false in _markBytecodeAsPublished
, so the if
clause will not be executed.
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/BytecodeCompressor.sol#L63
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/KnownCodesStorage.sol#L51
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/KnownCodesStorage.sol#L68
So, the parameter _l1PreimageHash
is not accessed at all, and can be removed.
If the processFlags
is something except 0x00
and 0x02
, isETHCall
is considered as false. So, it will be interpreted as normal mode (validate and execute).
https://github.com/code-423n4/2023-03-zksync/blob/01379615bc20c2926d81c0a182f1abb6e922b93a/bootloader/bootloader.yul#L579
It is recommended to use switch/case
to handle invalid cases.
In EVM, blockhash(block.number) = bytes32(0)
. In zKSync, when emulating the opcode blockhash
, it is better to explicitly define the case that _block
is equal to block.number
will results in bytes32(0).
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/SystemContext.sol#L74
Although if _block == block.number
, the else branch will be executed, and the mapping returns bytes32(0), but in case the blocks in L2 are reorged and the state of the blockhash is not set to zero, we may have blockHash[block.number] != 0
.
The recommendation is using <=
instead of <
:
function getBlockHashEVM(uint256 _block) external view returns (bytes32 hash) { if (block.number <= _block || block.number - _block > 256) { hash = bytes32(0); } else { hash = blockHash[_block]; } }
Or
function getBlockHashEVM(uint256 _block) external view returns (bytes32 hash) { if (block.number < _block || block.number - _block > 256) { hash = bytes32(0); } else { hash = blockHash[_block]; if(_block == block.number){ assert(hash == bytes32(0)); } } }
#0 - miladpiri
2023-03-27T13:26:12Z
All are more or less valid, but mostly will not be immplemented. Q6 is duplicate of https://github.com/code-423n4/2023-03-zksync-findings/issues/196 Q1, Q8, and Q9 are interesting.
#1 - GalloDaSballo
2023-03-31T09:38:27Z
L
NC
##Β The following check is not needed as there is casting to R
R
R
##Β If by chance the _newAddress during creating an account is in kernel space - Q6 TODO
R
TODO
R
Will prob give bonus for signal
#2 - GalloDaSballo
2023-04-06T18:16:22Z
If by chance the _newAddress during creating an account is in kernel space Disputing
If the processFlags is something except 0x00 and 0x02 R
2L from dups
1L 5R 1NC
1
#3 - GalloDaSballo
2023-04-06T18:16:29Z
3L 5R 1NC with dups
#4 - c4-judge
2023-04-06T18:59:16Z
GalloDaSballo marked the issue as grade-a