-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Users can avoid paying fees for failed L2 transactions #520
Comments
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, Sponsor comment:
The given dispute seems to confirm the vulnerability, let's continue with an example.
Impact: No matter if the transaction is in- or excluded on the next try, the user always wins by not paying fees for failure. Judge comment:
Seeing the transaction revert beforehand and not including it also does not lead to fee payment, which is normal/intended behaviour. 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. Thanks for reading and have a nice day, everyone! |
Generally, the scenario that the warden described is avoided by utilizing restrictions for account validation: |
I have to respectfully disagree, according to https://era.zksync.io/docs/reference/concepts/account-abstraction.html#extending-eip4337 during the validation step:
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:
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:
(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
The text was updated successfully, but these errors were encountered: