Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Valid

`call` work differently on zkSync Era, using it as is in solidity will lead to unexpected results

Summary

in _executeTransaction there is particular code block, which is using callmethod to send ether to target. However zksync era do not support passing native ether as of now.

Vulnerability Details

In _executeTransactionfunction, when to != DEPLOYER_SYSTEM_CONTRACT , it call the target with provided eth value

and data like how it's done on ethereum. As we can see highlighted code below.

function _executeTransaction(Transaction memory _transaction) internal {
address to = address(uint160(_transaction.to));
uint128 value = Utils.safeCastToU128(_transaction.value);
bytes memory data = _transaction.data;
if (to == address(DEPLOYER_SYSTEM_CONTRACT)) {
uint32 gas = Utils.safeCastToU32(gasleft());
SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data);
} else {
bool success;
@> (success,) = to.call{value: value}(data);
if (!success) {
revert MondrianWallet2__ExecutionFailed();
}
}
}

However call is not same as it works on ethereum. As per ZksyncEra Docs ,

  • There is no native support for Ether transfers within the call function.

  • Ether transfers are managed by a special system contract called MsgValueSimulator.

  • MsgValueSimulator receives the address of the callee and the Ether amount, performs the necessary balance changes, and then calls the callee.

Basic difference b/w ethereum and zkSync how call is handled
Ethereum
success := call(gas(), target, 0, in, insize, out, outsize) // grows to 'min(returndatasize(), out + outsize)'
  • 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.

Zksync
bool success = call(gas(), target, 0, in, insize, out, 0);
if (!success) revert("Call failed");
returndatacopy(out, 0, returndatasize());
  • zkSync Era performs memory allocation 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 Era might not trigger a panic in scenarios where EVM would, due to the difference in memory growth timing.

Impact

Unexpected behaviour of call on zksync could lead to failed txs, eth loss etc.

Tools Used

Manual Review

Recommendations

update the code as shown below to fix the issue.

function _executeTransaction(Transaction memory _transaction) internal {
address to = address(uint160(_transaction.to));
uint128 value = Utils.safeCastToU128(_transaction.value);
bytes memory data = _transaction.data;
if (to == address(DEPLOYER_SYSTEM_CONTRACT)) {
uint32 gas = Utils.safeCastToU32(gasleft());
SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data);
} else {
bool success;
- (success,) = to.call{value: value}(data);
+ assembly {
+ success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
+ }
if (!success) {
revert MondrianWallet2__ExecutionFailed();
}
}
}
Updates

Lead Judging Commences

bube Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Call works differently on ZKsync

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.