Summary
_executeTransaction
uses the CALL
opcode instead of an assembly block to make a call:
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();
}
}
}
But the CALL
opcode behaves differently on ZkSync than on Ethereum: https://www.rollup.codes/zksync-era
Impact
Unexpected behavior, failure to send ether.
If ether is to be sent via the CALL
opcode, system contract MsgValueSimulator
would have to be invoked but _executeTransaction
is not prepared to interact with this system contract.
Tools Used
Manual review, Foundry.
Recommendations
Make the call in the ZkSync way:
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();
}
}
}