Summary
MondrianWallet2::_executeTransaction
Does not correctly implement call for ZKsync.
Vulnerability Details
As per the ZKsync documentation, call works slightly differently than on Ethereum and thus needs to be implemented differently, generally using an assembly block.
function(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();
}
}
}
Impact
As per the ZKsync documentation:
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.
Tools Used
--Foundry
Recommendations
It is recommended to implement call as demonstrated in the ZKsync Era documentation.
function (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();
}
}
}