Closed code423n4 closed 1 year ago
GalloDaSballo marked the issue as primary issue
Very thorough report
That said, I believe that we'd need to also have demonstrated the breaking of the totalSupply == SUM(USERS.balance)
Meaning we should never be able to overflow in prod
The finalization of the withdrawal happens on L1. Moreover, tokens are minted 1:1 with ETH on L2.
The msg.value
increases the balance[address(this)] through transferFromTo.
miladpiri marked the issue as sponsor disputed
With the information I have, I believe the finding did not demonstrate a way to break the invariant of totalSupply == SUM(all balances)
This plus the fact that a call will be done via MsgValueSimulator, which will use transferFromTo
, meaning that the value will be transferred to the L2ETHToken and then burned via the subtraction, leads me to believe that the finding lacks proof
I recommend the warden to follow up with me (post judging QA) or the sponsor if they can demonstrate a way to break the above mentioned invariant, but this report did not provide sufficient evidence
GalloDaSballo marked the issue as unsatisfactory: Insufficient proof
Lines of code
https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/L2EthToken.sol#L85 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/L2EthToken.sol#L80 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/L2EthToken.sol#L84-L87 https://github.com/code-423n4/2023-03-zksync/blob/21d9a364a4a75adfa6f1e038232d8c0f39858a64/contracts/L2EthToken.sol#L72-L75
Vulnerability details
Impact
This can break accounting as well as create opportunities for abuse
Proof of Concept
1)
L2EthToken.mint()
has theonlyBootloader
modifier and can mint any amount for anyone:2)
L2EthToken.mint()
is totally unprotected and can be called by anyone, where a subtraction that is undocumented andunchecked
with theL2EthToken
contract's balance is happening:3)
L1_MESSENGER_CONTRACT.sendToL1(message)
is an external call to L1Messenger.sendToL1() which isn't a payable function. InsidesendToL1()
, msg.sender will be theL2EthToken
contract. This call should always work and will therefore be commented in the coded POC for the code to compile with Foundry.4) Use the following to setup a Foundry POC:
Setting up a hybrid project
Set the following:
And, unfortunately, for the project / current scope to compile with Foundry so that we can test what we want: we need to delete the
.yul
files undercontracts
(precompiles/
andEventWriter.yul
), otherwise we'll get an error telling us that the compiler only supports 1yul
file in the project.5) Replace the following so that the code can compile with Foundry. As stated before, this call should always succeed:
6) Inject the following coded POC under
2023-03-zksync/test/L2EthToken.t.sol
:7) Run the test with
forge test -m test_withdraw -vv
and see the following output:This effectively shows an underflow and could happen with a whale, or perhaps even with a flashloan (but that one can't be verified due to a lack of documentation on the L1's
finalizeEthWithdrawal
method, so we don't know if the flashloan could be repaid in the same transaction. The whale however can wait to retrieve their funds throughfinalizeEthWithdrawal
).Tools Used
Manual review, Foundry
Recommended Mitigation Steps
There's a strong feeling here that the following should've been in
withdraw()
:This way, a user could only
withdraw
the authorized amount that's been minted (or transferred) for them from the bootloader and thetotalSupply
would make more sense (totalSupply
being equal to the sum of allbalance
s is a classic invariant).Additionally,
balance[address(this)] -= amount;
would definitely underflow if all users decided towithdraw
and thatbalance[address(this)] >= totalSupply / 2
wasn't being enforced, which seems like quite a singular behavior that should be thoroughly documented.If the benefit of the doubt is given:
unchecked
statement should remain, consider documenting it and eventually adding some checks to prevent any underflow.balance[address(this)]
is somehow the right logic, consider documenting this thoroughly and enforce the fact that it will never underflow, probably with some strong initial minting (that I didn't see inbootloader.yul
).