Open c4-submissions opened 1 year ago
bytes032 marked the issue as primary issue
141345 marked the issue as sufficient quality report
Invalid, the operator would roll back this transaction and re-try.
miladpiri (sponsor) disputed
GalloDaSballo changed the severity to QA (Quality Assurance)
I agree with the Sponsor because in eth_calling the tx the operator would see it revert, this could create scenarios that make it hard to debug so I think QA is appropriate
Hi @GalloDaSballo,
It seems that the mechanics and impact of the present issue were misunderstood. I'll gladly take this as feedback to be more concise in future reports.
In the following, the issue is explained from another point of view by answering the sponsor's and your comment.
Concerning proof of the claims below, please refer to the PoC in the original report.
Sponsor comment:
The operator would roll back this transaction and re-try.
The given dispute seems to confirm the vulnerability, let's continue with an example.
Assume we are executing some "DeFi transaction" which can revert due to slippage or a deadline:
DefaultAccount
.
FeeSavingAccount
, see also PoC.
Impact:
Users of the custom FeeSavingAccount
will never under any circumstances pay fees for failed transactions due to causing a revert in the account's validation or payment step on failed execution.
Since paying for failed transactions is intended and part of the operators' income on zkSync Era, the present vulnerability causes loss of the latter while creating a "successful-transactions-only chain".
No matter if the transaction is in- or excluded on the next try, the user always wins by not paying fees for failure.
Consider the loss of fee income if this would be possible on Ethereum, and users could stop failed transactions from being included in block and escape the fee payment by reverting some internal mechanism.
See also: Over 1.2 Million Ethereum Transactions Failed in May (external link to news site)
Judge comment:
I agree with the Sponsor because in eth_calling the tx the operator would see it revert, this could create scenarios that make it hard to debug so I think QA is appropriate
Seeing the transaction revert beforehand and not including it also does not lead to fee payment, which is normal/intended behaviour.
However, there are transactions which successfully pass eth_call
/ gas estimation, but then fail during real execution due to e.g. a slippage check. Such transactions are still included in a block/batch and the transaction fee is paid. The present vulnerability avoids the fee payment in these cases.
For further clarification, let's take a look on the recently processed transactions on zkSync Era, or especially this one for example. One can see that failed transactions, which are still included in a block/batch, are not rare at all. In all those cases, the transaction fee is deduced as intended (EVM compatibility). The use case of the present vulnerability is to prevent the fee payment in such instances by reverting the bootloader.
Anyone can simply take advantage of this by using the FeeSavingAccount
.
Thanks for reading and have a nice day, everyone!
Generally, the scenario that the warden described is avoided by utilizing restrictions for account validation: https://era.zksync.io/docs/reference/concepts/account-abstraction.html#extending-eip4337 For instance, the example with slippage wouldn't work, because we don't allow accessing other contracts' data during validation. So yes, from the contracts' perspective it is possible, but it is a vulnerability (the operator will rollback the transaction and not even re-try, it'll just throw away the transaction from the mempool). The DDoS vector is prevented by the offchain validation methods
I have to respectfully disagree, according to https://era.zksync.io/docs/reference/concepts/account-abstraction.html#extending-eip4337 during the validation step:
It is allowed to
call
/delegateCall
/staticcall
contracts that were already deployed.
This allows to circumvent the data access limitation during validation by calling into another contract where the data can be accessed normally, see also PoC of #469 where the state of another contract is modified..
Moreover, see initial submission:
Alternatively, the execution could also be performed during the payment step (bootloader calls payForTransaction(...) method) to achieve the same outcome.
This explains why the described scenario is possible while it's also proven by the PoC in #469 that state-changing interactions with other contracts during the validation or payment step are not restricted.
Furthermore, throwing away such a failed transaction from the mempool is desired by the (ab-)user of this vulnerability in order to escape the fees on failure.
Edit: Although not pointed out in the initial submission, a transaction could also be executed through a custom paymaster since it also gets the necessary transaction data, while no meaningful restrictions can be imposed at this step, because:
While ETH is the formal fee token in zkSync, paymasters can provide the ability to exchange ERC20 tokens to ETH on the fly.
(see https://era.zksync.io/docs/reference/concepts/account-abstraction.html#paymasters)
All in alll, it seems evident that the discussed exploit cannot be easily avoided.
While the other finding has stronger legs, this one ultimately will not have the same impact as any data passed by the CustomAccount would be already known, hence the validation could have been done offChain and not particular advantage is gained in performing it on zkSync
I think QA is most appropriate here, but I'll flag to do one final review
Lines of code
https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L2191-L2222 https://github.com/code-423n4/2023-10-zksync/blob/1fb4649b612fac7b4ee613df6f6b7d921ddd6b0d/code/system-contracts/bootloader/bootloader.yul#L1994-L2042
Vulnerability details
Impact
On Ethereum, even for failed transactions (execution reverted at some point) a gas fee is charged. zkSync Era adopted this principle and implemented this by not reverting the whole bootloader when a L2 transaction execution has failed. The failure/success is simply recorded at the result pointer for further processing and the transaction is finalized. This way, transaction fees are paid in either case.
However, under the hood zkSync Era uses native account abstraction, see documentation and DefaultAccount.sol, to facilitate L2 transactions. Moreover, before executing a transaction through an account, the bootloader performs a (signature) validation step which indeed reverts the whole bootloader on failure in contrast to the execution step.
As a consquence, everyone can deploy a custom account which already performs the execution within the validation step (bootloader calls
validateTransaction(...)
method) and does nothing during the execution step (bootloader callsexecuteTransaction(...)
method).This results in the following behaviour:
DefaultAccount
and transaction fees are paid.Alternatively, the execution could also be performed during the payment step (bootloader calls
payForTransaction(...)
method) to achieve the same outcome.Proof of Concept
The following PoC demonstrates that the payment of transaction fees on execution failure can be easily avoided using the FeeSavingAccount.sol (secret gist), which performs the execution already on validation. Furthermore, the StingyContract.sol (secret gist), which serves as a (non-)reverting transaction target, is required for this PoC. Please put those contracts into
./code/system-contracts/contracts/test-contracts
.Apply the diff below to the existing
DefaultAccount
test suite and runbash quick-setup.sh
from./code/system-contracts/scripts
to execute the PoC test cases:Tools Used
Manual review
Recommended Mitigation Steps
staticcall
from the bootloader to enforce that the execution step cannot be performed there, i.e. no state changes can be made. TheDefaultAccount
needs to be adapted accordingly.Assessed type
Other