Several functions in the protocol call Rescuable::_safe_transfer
or Rescuable::_safe_transfer_from
functions. But these functions will not work properly on ZKsync Era
chain.
The TokenManager::tillIn
, TokenManager::withdraw
and TokenManager::_transfer
functions rely on Rescuable::_safe_transfer
or Rescuable::_safe_transfer_from
functions:
TokenManager::tillIn
:
TokenManager::withdraw
:
TokenManager::_transfer
:
The Rescuable
contract is not in scope but the _safe_transfer
and _safe_transfer_from
functions are used in scope in very important for the protocol functionality functions. So the issues related to these two functions will break the protocol functionality.
Let's consider the _safe_transfer
and _safe_transfer_from
functions:
The purpose of these functions is to transfer a given token or ethers. According to the documentation of the contest the protocol should be compatible with every EVM-compatible chain:
The problem is that the _safe_transfer
and _safe_transfer_from
functions transfer tokens or ethers using the call
function. But the way in which they use the call
function will not work on ZKsync
chain. The call
on ZKsync
chain works differently according to the docs:
https://docs.zksync.io/build/developer-reference/ethereum-differences/evm-instructions#call-staticcall-delegatecall
For calls, you specify a memory slice to write the return data to, e.g. out and outsize arguments for call(g, a, v, in, insize, out, outsize). In EVM, if outsize != 0, the allocated memory will grow to out + outsize (rounded up to the words) regardless of the returndatasize. On ZKsync Era, returndatacopy, similar to calldatacopy, is implemented as a cycle iterating over return data with a few additional checks and triggering a panic if out + outsize > returndatasize to simulate the same behavior as in EVM.
Thus, unlike EVM where memory growth occurs before the call itself, on ZKsync Era, the necessary copying of return data happens only after the call has ended, leading to a difference in msize() and sometimes ZKsync Era not panicking where EVM would panic due to the difference in memory growth.
Additionally, there is no native support for passing Ether on ZKsync Era, so it is handled by a special system contract called MsgValueSimulator. The simulator receives the callee address and Ether amount, performs all necessary balance changes, and then calls the callee.
Let's take a look on a differences between call
on Ethereum
and on ZKsync
:
In Ethereum
when using call(gas(), target, value, in, insize, out, outsize)
, the EVM will allocate memory up to out + outsize
regardless of the actual size of - the returned data (returndatasize). This allocation happens before the call is made. If outsize is non-zero, the memory growth occurs immediately.
In ZKsync
the memory allocation is performed for return data only after the call ends. Instead of pre-allocating memory, ZKsync Era
copies the return data with additional checks to ensure that out + outsize
does not exceed returndatasize
. This means msize()
(memory size) differs between ZKsync Era
and EVM during the call, and ZKsync
might not trigger a panic in scenarios where EVM would, due to the difference in memory growth timing.
Also, if the functions should work with ethers
like the comments describe, that will be impossible on ZKsync
. That is because there is no native support for passing Ether
on ZKsync Era
, so it is handled by a special system contract called MsgValueSimulator
.
The different behaviour of call
method on ZKsync Era
chain could lead to failed transactions and loss funds.
Certain calls that should fail due to excessive memory usage might initially appear successful but will eventually fail due to an EVM error. Conversely, because the _safe_transfer
and _safe_transfer_from
functions store returned data in memory, some operations that should succeed might end up failing.
Manual Review
Use the assembly call
instead of the standard .call
that limits the return data to length 0.
I would require a more explicit example on zkSync mainnet to prove that such a low level call is indeed an issue, i.e. a example live contract on zkSync that shows the usual low level `call()` cannot be utilized.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.