diff --git a/v2/contracts/evm/GatewayEVM.sol b/v2/contracts/evm/GatewayEVM.sol index 18a17669..e1a1a9a3 100644 --- a/v2/contracts/evm/GatewayEVM.sol +++ b/v2/contracts/evm/GatewayEVM.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.26; import { RevertContext, RevertOptions, Revertable } from "../../contracts/Revert.sol"; import { ZetaConnectorBase } from "./ZetaConnectorBase.sol"; import { IERC20Custody } from "./interfaces/IERC20Custody.sol"; -import { IGatewayEVM } from "./interfaces/IGatewayEVM.sol"; +import { Callable, IGatewayEVM, MessageContext } from "./interfaces/IGatewayEVM.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -71,17 +71,6 @@ contract GatewayEVM is /// @param newImplementation Address of the new implementation. function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } - /// @dev Internal function to execute a call to a destination address. - /// @param destination Address to call. - /// @param data Calldata to pass to the call. - /// @return The result of the call. - function _execute(address destination, bytes calldata data) internal returns (bytes memory) { - (bool success, bytes memory result) = destination.call{ value: msg.value }(data); - if (!success) revert ExecutionFailed(); - - return result; - } - /// @notice Pause contract. function pause() external onlyRole(PAUSER_ROLE) { _pause(); @@ -115,12 +104,14 @@ contract GatewayEVM is emit Reverted(destination, address(0), msg.value, data, revertContext); } - /// @notice Executes a call to a destination address without ERC20 tokens. + /// @notice Executes an authenticated call to a destination address without ERC20 tokens. /// @dev This function can only be called by the TSS address and it is payable. + /// @param messageContext Message context containing sender. /// @param destination Address to call. /// @param data Calldata to pass to the call. /// @return The result of the call. function execute( + MessageContext calldata messageContext, address destination, bytes calldata data ) @@ -132,7 +123,31 @@ contract GatewayEVM is returns (bytes memory) { if (destination == address(0)) revert ZeroAddress(); - bytes memory result = _execute(destination, data); + bytes memory result; + result = _executeAuthenticatedCall(messageContext, destination, data); + + emit Executed(destination, msg.value, data); + + return result; + } + + /// @notice Executes an arbitrary call to a destination address without ERC20 tokens. + /// @dev This function can only be called by the TSS address and it is payable. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function execute( + address destination, + bytes calldata data + ) + external + payable + onlyRole(TSS_ROLE) + whenNotPaused + returns (bytes memory) + { + if (destination == address(0)) revert ZeroAddress(); + bytes memory result = _executeArbitraryCall(destination, data); emit Executed(destination, msg.value, data); @@ -163,7 +178,7 @@ contract GatewayEVM is if (!resetApproval(token, to)) revert ApprovalFailed(); if (!IERC20(token).approve(to, amount)) revert ApprovalFailed(); // Execute the call on the target contract - _execute(to, data); + _executeArbitraryCall(to, data); // Reset approval if (!resetApproval(token, to)) revert ApprovalFailed(); @@ -385,4 +400,46 @@ contract GatewayEVM is IERC20(token).safeTransfer(custody, amount); } } + + /// @dev Private function to execute an arbitrary call to a destination address. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function _executeArbitraryCall(address destination, bytes calldata data) private returns (bytes memory) { + revertIfAuthenticatedCall(data); + (bool success, bytes memory result) = destination.call{ value: msg.value }(data); + if (!success) revert ExecutionFailed(); + + return result; + } + + /// @dev Private function to execute an authenticated call to a destination address. + /// @param messageContext Message context containing sender and arbitrary call flag. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function _executeAuthenticatedCall( + MessageContext calldata messageContext, + address destination, + bytes calldata data + ) + private + returns (bytes memory) + { + return Callable(destination).onCall(messageContext, data); + } + + // @dev prevent calling onCall function reserved for authenticated calls + function revertIfAuthenticatedCall(bytes calldata data) private pure { + if (data.length >= 4) { + bytes4 functionSelector; + assembly { + functionSelector := calldataload(data.offset) + } + + if (functionSelector == Callable.onCall.selector) { + revert NotAllowedToCallOnCall(); + } + } + } } diff --git a/v2/contracts/evm/interfaces/IGatewayEVM.sol b/v2/contracts/evm/interfaces/IGatewayEVM.sol index ff91df1f..d1f4dcb9 100644 --- a/v2/contracts/evm/interfaces/IGatewayEVM.sol +++ b/v2/contracts/evm/interfaces/IGatewayEVM.sol @@ -80,6 +80,9 @@ interface IGatewayEVMErrors { /// @notice Error when trying to transfer not whitelisted token to custody. error NotWhitelistedInCustody(); + + /// @notice Error when trying to call onCall method using arbitrary call. + error NotAllowedToCallOnCall(); } /// @title IGatewayEVM @@ -111,6 +114,21 @@ interface IGatewayEVM is IGatewayEVMErrors, IGatewayEVMEvents { /// @return The result of the contract call. function execute(address destination, bytes calldata data) external payable returns (bytes memory); + /// @notice Executes a call to a destination address without ERC20 tokens. + /// @dev This function can only be called by the TSS address and it is payable. + /// @param messageContext Message context containing sender and arbitrary call flag. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function execute( + MessageContext calldata messageContext, + address destination, + bytes calldata data + ) + external + payable + returns (bytes memory); + /// @notice Executes a revertable call to a contract using ERC20 tokens. /// @param token The address of the ERC20 token. /// @param to The address of the contract to call. @@ -171,3 +189,14 @@ interface IGatewayEVM is IGatewayEVMErrors, IGatewayEVMEvents { /// @param revertOptions Revert options. function call(address receiver, bytes calldata payload, RevertOptions calldata revertOptions) external; } + +/// @notice Message context passed to execute function. +/// @param sender Sender from omnichain contract. +struct MessageContext { + address sender; +} + +/// @notice Interface implemented by contracts receiving authenticated calls. +interface Callable { + function onCall(MessageContext calldata context, bytes calldata message) external returns (bytes memory); +} diff --git a/v2/contracts/zevm/GatewayZEVM.sol b/v2/contracts/zevm/GatewayZEVM.sol index a00186bb..18d47dd8 100644 --- a/v2/contracts/zevm/GatewayZEVM.sol +++ b/v2/contracts/zevm/GatewayZEVM.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { IGatewayZEVM } from "./interfaces/IGatewayZEVM.sol"; +import { CallOptions, IGatewayZEVM } from "./interfaces/IGatewayZEVM.sol"; import { RevertContext, RevertOptions } from "../../contracts/Revert.sol"; import "./interfaces/IWZETA.sol"; @@ -151,7 +151,7 @@ contract GatewayZEVM is gasFee, IZRC20(zrc20).PROTOCOL_FLAT_FEE(), "", - IZRC20(zrc20).GAS_LIMIT(), + CallOptions({ gasLimit: IZRC20(zrc20).GAS_LIMIT(), isArbitraryCall: true }), revertOptions ); } @@ -188,7 +188,44 @@ contract GatewayZEVM is gasFee, IZRC20(zrc20).PROTOCOL_FLAT_FEE(), message, - gasLimit, + CallOptions({ gasLimit: gasLimit, isArbitraryCall: true }), + revertOptions + ); + } + + /// @notice Withdraw ZRC20 tokens and call a smart contract on an external chain. + /// @param receiver The receiver address on the external chain. + /// @param amount The amount of tokens to withdraw. + /// @param zrc20 The address of the ZRC20 token. + /// @param message The calldata to pass to the contract call. + /// @param callOptions Call options including gas limit and arbirtrary call flag. + /// @param revertOptions Revert options. + function withdrawAndCall( + bytes memory receiver, + uint256 amount, + address zrc20, + bytes calldata message, + CallOptions calldata callOptions, + RevertOptions calldata revertOptions + ) + external + nonReentrant + whenNotPaused + { + if (receiver.length == 0) revert ZeroAddress(); + if (amount == 0) revert InsufficientZRC20Amount(); + + uint256 gasFee = _withdrawZRC20WithGasLimit(amount, zrc20, callOptions.gasLimit); + emit Withdrawn( + msg.sender, + 0, + receiver, + zrc20, + amount, + gasFee, + IZRC20(zrc20).PROTOCOL_FLAT_FEE(), + message, + callOptions, revertOptions ); } @@ -211,7 +248,18 @@ contract GatewayZEVM is if (amount == 0) revert InsufficientZetaAmount(); _transferZETA(amount, FUNGIBLE_MODULE_ADDRESS); - emit Withdrawn(msg.sender, chainId, receiver, address(zetaToken), amount, 0, 0, "", 0, revertOptions); + emit Withdrawn( + msg.sender, + chainId, + receiver, + address(zetaToken), + amount, + 0, + 0, + "", + CallOptions({ gasLimit: 0, isArbitraryCall: true }), + revertOptions + ); } /// @notice Withdraw ZETA tokens and call a smart contract on an external chain. @@ -235,7 +283,66 @@ contract GatewayZEVM is if (amount == 0) revert InsufficientZetaAmount(); _transferZETA(amount, FUNGIBLE_MODULE_ADDRESS); - emit Withdrawn(msg.sender, chainId, receiver, address(zetaToken), amount, 0, 0, message, 0, revertOptions); + emit Withdrawn( + msg.sender, + chainId, + receiver, + address(zetaToken), + amount, + 0, + 0, + message, + CallOptions({ gasLimit: 0, isArbitraryCall: true }), + revertOptions + ); + } + + /// @notice Withdraw ZETA tokens and call a smart contract on an external chain. + /// @param receiver The receiver address on the external chain. + /// @param amount The amount of tokens to withdraw. + /// @param chainId Chain id of the external chain. + /// @param message The calldata to pass to the contract call. + /// @param callOptions Call options including gas limit and arbirtrary call flag. + /// @param revertOptions Revert options. + function withdrawAndCall( + bytes memory receiver, + uint256 amount, + uint256 chainId, + bytes calldata message, + CallOptions calldata callOptions, + RevertOptions calldata revertOptions + ) + external + nonReentrant + whenNotPaused + { + if (receiver.length == 0) revert ZeroAddress(); + if (amount == 0) revert InsufficientZetaAmount(); + + _transferZETA(amount, FUNGIBLE_MODULE_ADDRESS); + emit Withdrawn( + msg.sender, chainId, receiver, address(zetaToken), amount, 0, 0, message, callOptions, revertOptions + ); + } + + /// @notice Call a smart contract on an external chain without asset transfer. + /// @param receiver The receiver address on the external chain. + /// @param zrc20 Address of zrc20 to pay fees. + /// @param message The calldata to pass to the contract call. + /// @param callOptions Call options including gas limit and arbirtrary call flag. + /// @param revertOptions Revert options. + function call( + bytes memory receiver, + address zrc20, + bytes calldata message, + CallOptions calldata callOptions, + RevertOptions calldata revertOptions + ) + external + nonReentrant + whenNotPaused + { + _call(receiver, zrc20, message, callOptions, revertOptions); } /// @notice Call a smart contract on an external chain without asset transfer. @@ -254,16 +361,28 @@ contract GatewayZEVM is external nonReentrant whenNotPaused + { + _call(receiver, zrc20, message, CallOptions({ gasLimit: gasLimit, isArbitraryCall: true }), revertOptions); + } + + function _call( + bytes memory receiver, + address zrc20, + bytes calldata message, + CallOptions memory callOptions, + RevertOptions memory revertOptions + ) + internal { if (receiver.length == 0) revert ZeroAddress(); if (message.length == 0) revert EmptyMessage(); - (address gasZRC20, uint256 gasFee) = IZRC20(zrc20).withdrawGasFeeWithGasLimit(gasLimit); + (address gasZRC20, uint256 gasFee) = IZRC20(zrc20).withdrawGasFeeWithGasLimit(callOptions.gasLimit); if (!IZRC20(gasZRC20).transferFrom(msg.sender, FUNGIBLE_MODULE_ADDRESS, gasFee)) { revert GasFeeTransferFailed(); } - emit Called(msg.sender, zrc20, receiver, message, gasLimit, revertOptions); + emit Called(msg.sender, zrc20, receiver, message, callOptions, revertOptions); } /// @notice Deposit foreign coins into ZRC20. diff --git a/v2/contracts/zevm/interfaces/IGatewayZEVM.sol b/v2/contracts/zevm/interfaces/IGatewayZEVM.sol index 0cbb0195..73108d3b 100644 --- a/v2/contracts/zevm/interfaces/IGatewayZEVM.sol +++ b/v2/contracts/zevm/interfaces/IGatewayZEVM.sol @@ -12,14 +12,14 @@ interface IGatewayZEVMEvents { /// @param zrc20 Address of zrc20 to pay fees. /// @param receiver The receiver address on the external chain. /// @param message The calldata passed to the contract call. - /// @param gasLimit Gas limit. + /// @param callOptions Call options including gas limit and arbirtrary call flag. /// @param revertOptions Revert options. event Called( address indexed sender, address indexed zrc20, bytes receiver, bytes message, - uint256 gasLimit, + CallOptions callOptions, RevertOptions revertOptions ); @@ -32,7 +32,7 @@ interface IGatewayZEVMEvents { /// @param gasfee The gas fee for the withdrawal. /// @param protocolFlatFee The protocol flat fee for the withdrawal. /// @param message The calldata passed to the contract call. - /// @param gasLimit Gas limit. + /// @param callOptions Call options including gas limit and arbirtrary call flag. /// @param revertOptions Revert options. event Withdrawn( address indexed sender, @@ -43,7 +43,7 @@ interface IGatewayZEVMEvents { uint256 gasfee, uint256 protocolFlatFee, bytes message, - uint256 gasLimit, + CallOptions callOptions, RevertOptions revertOptions ); } @@ -134,6 +134,23 @@ interface IGatewayZEVM is IGatewayZEVMErrors, IGatewayZEVMEvents { ) external; + /// @notice Withdraw ZRC20 tokens and call a smart contract on an external chain. + /// @param receiver The receiver address on the external chain. + /// @param amount The amount of tokens to withdraw. + /// @param zrc20 The address of the ZRC20 token. + /// @param message The calldata to pass to the contract call. + /// @param callOptions Call options including gas limit and arbirtrary call flag. + /// @param revertOptions Revert options. + function withdrawAndCall( + bytes memory receiver, + uint256 amount, + address zrc20, + bytes calldata message, + CallOptions calldata callOptions, + RevertOptions calldata revertOptions + ) + external; + /// @notice Withdraw ZETA tokens and call a smart contract on an external chain. /// @param receiver The receiver address on the external chain. /// @param amount The amount of tokens to withdraw. @@ -149,6 +166,38 @@ interface IGatewayZEVM is IGatewayZEVMErrors, IGatewayZEVMEvents { ) external; + /// @notice Withdraw ZETA tokens and call a smart contract on an external chain. + /// @param receiver The receiver address on the external chain. + /// @param amount The amount of tokens to withdraw. + /// @param chainId Chain id of the external chain. + /// @param message The calldata to pass to the contract call. + /// @param callOptions Call options including gas limit and arbirtrary call flag. + /// @param revertOptions Revert options. + function withdrawAndCall( + bytes memory receiver, + uint256 amount, + uint256 chainId, + bytes calldata message, + CallOptions calldata callOptions, + RevertOptions calldata revertOptions + ) + external; + + /// @notice Call a smart contract on an external chain without asset transfer. + /// @param receiver The receiver address on the external chain. + /// @param zrc20 Address of zrc20 to pay fees. + /// @param message The calldata to pass to the contract call. + /// @param callOptions Call options including gas limit and arbirtrary call flag. + /// @param revertOptions Revert options. + function call( + bytes memory receiver, + address zrc20, + bytes calldata message, + CallOptions calldata callOptions, + RevertOptions calldata revertOptions + ) + external; + /// @notice Call a smart contract on an external chain without asset transfer. /// @param receiver The receiver address on the external chain. /// @param zrc20 Address of zrc20 to pay fees. @@ -231,3 +280,11 @@ interface IGatewayZEVM is IGatewayZEVMErrors, IGatewayZEVMEvents { ) external; } + +/// @notice CallOptions struct passed to call and withdrawAndCall functions. +/// @param gasLimit Gas limit. +/// @param isArbitraryCall Indicates if call should be arbitrary or authenticated. +struct CallOptions { + uint256 gasLimit; + bool isArbitraryCall; +} diff --git a/v2/test/GatewayEVM.t.sol b/v2/test/GatewayEVM.t.sol index 11932ee4..77163435 100644 --- a/v2/test/GatewayEVM.t.sol +++ b/v2/test/GatewayEVM.t.sol @@ -137,6 +137,15 @@ contract GatewayEVMTest is Test, IGatewayEVMErrors, IGatewayEVMEvents, IReceiver gateway.execute(address(receiver), data); } + function testForwardCallToReceiveOnCallUsingAuthCall() public { + vm.expectEmit(true, true, true, true, address(receiver)); + emit ReceivedOnCall(); + vm.expectEmit(true, true, true, true, address(gateway)); + emit Executed(address(receiver), 0, bytes("1")); + vm.prank(tssAddress); + gateway.execute(MessageContext({ sender: address(0x123) }), address(receiver), bytes("1")); + } + function testForwardCallToReceiveNonPayableFailsIfSenderIsNotTSS() public { string[] memory str = new string[](1); str[0] = "Hello, Foundry!"; @@ -150,6 +159,19 @@ contract GatewayEVMTest is Test, IGatewayEVMErrors, IGatewayEVMEvents, IReceiver gateway.execute(address(receiver), data); } + function testForwardCallToReceiveNonPayableWithMsgContextFailsIfSenderIsNotTSS() public { + string[] memory str = new string[](1); + str[0] = "Hello, Foundry!"; + uint256[] memory num = new uint256[](1); + num[0] = 42; + bool flag = true; + bytes memory data = abi.encodeWithSignature("receiveNonPayable(string[],uint256[],bool)", str, num, flag); + + vm.prank(owner); + vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, owner, TSS_ROLE)); + gateway.execute(MessageContext({ sender: address(0x123) }), address(receiver), data); + } + function testForwardCallToReceivePayable() public { string memory str = "Hello, Foundry!"; uint256 num = 42; @@ -182,6 +204,14 @@ contract GatewayEVMTest is Test, IGatewayEVMErrors, IGatewayEVMEvents, IReceiver gateway.execute(address(receiver), data); } + function testForwardCallToReceiveOnCallFails() public { + bytes memory data = abi.encodeWithSignature("onCall((address),bytes)", address(123), bytes("")); + + vm.prank(tssAddress); + vm.expectRevert(NotAllowedToCallOnCall.selector); + gateway.execute(address(receiver), data); + } + function testExecuteFailsIfDestinationIsZeroAddress() public { bytes memory data = abi.encodeWithSignature("receiveNoParams()"); @@ -190,6 +220,14 @@ contract GatewayEVMTest is Test, IGatewayEVMErrors, IGatewayEVMEvents, IReceiver gateway.execute(address(0), data); } + function testExecuteWithMsgContextFailsIfDestinationIsZeroAddress() public { + bytes memory data = abi.encodeWithSignature("receiveNoParams()"); + + vm.prank(tssAddress); + vm.expectRevert(ZeroAddress.selector); + gateway.execute(MessageContext({ sender: address(0x123) }), address(0), data); + } + function testForwardCallToReceiveNoParamsTogglePause() public { vm.prank(tssAddress); vm.expectRevert(abi.encodeWithSelector(AccessControlUnauthorizedAccount.selector, tssAddress, PAUSER_ROLE)); diff --git a/v2/test/GatewayEVMZEVM.t.sol b/v2/test/GatewayEVMZEVM.t.sol index a81ab8e3..f4af846f 100644 --- a/v2/test/GatewayEVMZEVM.t.sol +++ b/v2/test/GatewayEVMZEVM.t.sol @@ -13,12 +13,11 @@ import "./utils/TestERC20.sol"; import "./utils/SenderZEVM.sol"; -import "./utils/SystemContractMock.sol"; +import { SystemContractMock } from "./utils/SystemContractMock.sol"; import { GatewayZEVM } from "../contracts/zevm/GatewayZEVM.sol"; import { IGatewayZEVM } from "../contracts/zevm/GatewayZEVM.sol"; -import { IGatewayZEVMErrors } from "../contracts/zevm/interfaces/IGatewayZEVM.sol"; -import { IGatewayZEVMEvents } from "../contracts/zevm/interfaces/IGatewayZEVM.sol"; +import { CallOptions, IGatewayZEVMErrors, IGatewayZEVMEvents } from "../contracts/zevm/interfaces/IGatewayZEVM.sol"; import { IGatewayEVMErrors } from "../contracts/evm/interfaces/IGatewayEVM.sol"; import { IGatewayEVMEvents } from "../contracts/evm/interfaces/IGatewayEVM.sol"; @@ -135,7 +134,14 @@ contract GatewayEVMZEVMTest is bytes memory message = abi.encodeWithSelector(receiverEVM.receivePayable.selector, str, num, flag); vm.prank(ownerZEVM); vm.expectEmit(true, true, true, true, address(gatewayZEVM)); - emit Called(address(ownerZEVM), address(zrc20), abi.encodePacked(receiverEVM), message, 1, revertOptions); + emit Called( + address(ownerZEVM), + address(zrc20), + abi.encodePacked(receiverEVM), + message, + CallOptions({ gasLimit: 1, isArbitraryCall: true }), + revertOptions + ); gatewayZEVM.call(abi.encodePacked(receiverEVM), address(zrc20), message, 1, revertOptions); // Call execute on evm @@ -195,11 +201,18 @@ contract GatewayEVMZEVMTest is expectedGasFee, zrc20.PROTOCOL_FLAT_FEE(), message, - 1, + CallOptions({ gasLimit: 1, isArbitraryCall: true }), revertOptions ); vm.prank(ownerZEVM); - gatewayZEVM.withdrawAndCall(abi.encodePacked(receiverEVM), 500_000, address(zrc20), message, 1, revertOptions); + gatewayZEVM.withdrawAndCall( + abi.encodePacked(receiverEVM), + 500_000, + address(zrc20), + message, + CallOptions({ gasLimit: 1, isArbitraryCall: true }), + revertOptions + ); // Check the balance after withdrawal uint256 balanceOfAfterWithdrawal = zrc20.balanceOf(ownerZEVM); diff --git a/v2/test/GatewayZEVM.t.sol b/v2/test/GatewayZEVM.t.sol index ee91ab4d..1e54b195 100644 --- a/v2/test/GatewayZEVM.t.sol +++ b/v2/test/GatewayZEVM.t.sol @@ -27,6 +27,7 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors address addr1; address fungibleModule; RevertOptions revertOptions; + CallOptions callOptions; error ZeroAddress(); error LowBalance(); @@ -71,6 +72,8 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors revertMessage: "", onRevertGasLimit: 0 }); + + callOptions = CallOptions({ gasLimit: 1, isArbitraryCall: true }); } function testWithdrawZRC20() public { @@ -87,7 +90,7 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors 0, zrc20.PROTOCOL_FLAT_FEE(), "", - 0, + CallOptions({ gasLimit: 0, isArbitraryCall: true }), revertOptions ); gateway.withdraw(abi.encodePacked(addr1), amount, address(zrc20), revertOptions); @@ -189,7 +192,62 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors expectedGasFee, zrc20.PROTOCOL_FLAT_FEE(), message, - gasLimit, + CallOptions({ gasLimit: gasLimit, isArbitraryCall: true }), + revertOptions + ); + gateway.withdrawAndCall(abi.encodePacked(addr1), amount, address(zrc20), message, gasLimit, revertOptions); + + uint256 ownerBalanceAfter = zrc20.balanceOf(owner); + assertEq(ownerBalanceBefore - amount - expectedGasFee, ownerBalanceAfter); + } + + function testWithdrawAndCallZRC20WithCallOptsFailsIfReceiverIsZeroAddress() public { + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectRevert(ZeroAddress.selector); + gateway.withdrawAndCall(abi.encodePacked(""), 1, address(zrc20), message, callOptions, revertOptions); + } + + function testWithdrawAndCallZRC20WithCallOptsFailsIfAmountIsZero() public { + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectRevert(InsufficientZRC20Amount.selector); + gateway.withdrawAndCall(abi.encodePacked(addr1), 0, address(zrc20), message, callOptions, revertOptions); + } + + function testWithdrawZRC20WithMessageWithCallOptsFailsIfNoAllowance() public { + uint256 amount = 1; + uint256 ownerBalanceBefore = zrc20.balanceOf(owner); + + // Remove allowance for gateway + vm.prank(owner); + zrc20.approve(address(gateway), 0); + + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectRevert(); + gateway.withdrawAndCall(abi.encodePacked(addr1), amount, address(zrc20), message, callOptions, revertOptions); + + // Check that balance didn't change + uint256 ownerBalanceAfter = zrc20.balanceOf(owner); + assertEq(ownerBalanceBefore, ownerBalanceAfter); + } + + function testWithdrawZRC20WithCallOptsWithMessage() public { + uint256 amount = 1; + uint256 ownerBalanceBefore = zrc20.balanceOf(owner); + + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + uint256 expectedGasFee = 1; + uint256 gasLimit = 1; + vm.expectEmit(true, true, true, true, address(gateway)); + emit Withdrawn( + owner, + 0, + abi.encodePacked(addr1), + address(zrc20), + amount, + expectedGasFee, + zrc20.PROTOCOL_FLAT_FEE(), + message, + CallOptions({ gasLimit: gasLimit, isArbitraryCall: true }), revertOptions ); gateway.withdrawAndCall(abi.encodePacked(addr1), amount, address(zrc20), message, gasLimit, revertOptions); @@ -220,6 +278,18 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors gateway.withdrawAndCall(abi.encodePacked(""), 1, 1, message, revertOptions); } + function testWithdrawAndCallZETAWithCallOptsFailsIfAmountIsZero() public { + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectRevert(InsufficientZetaAmount.selector); + gateway.withdrawAndCall(abi.encodePacked(addr1), 0, 1, message, callOptions, revertOptions); + } + + function testWithdrawAndCallZETAWithCallOptsFailsIfAmountIsReceiverIsZeroAddress() public { + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectRevert(ZeroAddress.selector); + gateway.withdrawAndCall(abi.encodePacked(""), 1, 1, message, callOptions, revertOptions); + } + function testWithdrawZETA() public { uint256 amount = 1; uint256 ownerBalanceBefore = zetaToken.balanceOf(owner); @@ -228,7 +298,18 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors uint256 chainId = 1; vm.expectEmit(true, true, true, true, address(gateway)); - emit Withdrawn(owner, chainId, abi.encodePacked(addr1), address(zetaToken), amount, 0, 0, "", 0, revertOptions); + emit Withdrawn( + owner, + chainId, + abi.encodePacked(addr1), + address(zetaToken), + amount, + 0, + 0, + "", + CallOptions({ gasLimit: 0, isArbitraryCall: true }), + revertOptions + ); gateway.withdraw(abi.encodePacked(addr1), amount, chainId, revertOptions); uint256 ownerBalanceAfter = zetaToken.balanceOf(owner); @@ -285,7 +366,16 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors vm.expectEmit(true, true, true, true, address(gateway)); emit Withdrawn( - owner, chainId, abi.encodePacked(addr1), address(zetaToken), amount, 0, 0, message, 0, revertOptions + owner, + chainId, + abi.encodePacked(addr1), + address(zetaToken), + amount, + 0, + 0, + message, + CallOptions({ gasLimit: 0, isArbitraryCall: true }), + revertOptions ); gateway.withdrawAndCall(abi.encodePacked(addr1), amount, chainId, message, revertOptions); @@ -324,6 +414,64 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors assertEq(fungibleModuleBalanceBefore, fungibleModule.balance); } + function testWithdrawZETAWithCallOptsWithMessage() public { + uint256 amount = 1; + uint256 ownerBalanceBefore = zetaToken.balanceOf(owner); + uint256 gatewayBalanceBefore = zetaToken.balanceOf(address(gateway)); + uint256 fungibleModuleBalanceBefore = fungibleModule.balance; + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + uint256 chainId = 1; + + vm.expectEmit(true, true, true, true, address(gateway)); + emit Withdrawn( + owner, + chainId, + abi.encodePacked(addr1), + address(zetaToken), + amount, + 0, + 0, + message, + callOptions, + revertOptions + ); + gateway.withdrawAndCall(abi.encodePacked(addr1), amount, chainId, message, callOptions, revertOptions); + + uint256 ownerBalanceAfter = zetaToken.balanceOf(owner); + assertEq(ownerBalanceBefore - 1, ownerBalanceAfter); + + uint256 gatewayBalanceAfter = zetaToken.balanceOf(address(gateway)); + assertEq(gatewayBalanceBefore, gatewayBalanceAfter); + + // Verify amount is transfered to fungible module + assertEq(fungibleModuleBalanceBefore + 1, fungibleModule.balance); + } + + function testWithdrawZETAWithCallOptsWithMessageFailsIfNoAllowance() public { + uint256 amount = 1; + uint256 ownerBalanceBefore = zetaToken.balanceOf(owner); + uint256 gatewayBalanceBefore = zetaToken.balanceOf(address(gateway)); + uint256 fungibleModuleBalanceBefore = fungibleModule.balance; + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + uint256 chainId = 1; + + // Remove allowance for gateway + vm.prank(owner); + zetaToken.approve(address(gateway), 0); + + vm.expectRevert(); + gateway.withdrawAndCall(abi.encodePacked(addr1), amount, chainId, message, callOptions, revertOptions); + + // Verify balances not changed + uint256 ownerBalanceAfter = zetaToken.balanceOf(owner); + assertEq(ownerBalanceBefore, ownerBalanceAfter); + + uint256 gatewayBalanceAfter = zetaToken.balanceOf(address(gateway)); + assertEq(gatewayBalanceBefore, gatewayBalanceAfter); + + assertEq(fungibleModuleBalanceBefore, fungibleModule.balance); + } + function testCallFailsIfReceiverIsZeroAddress() public { bytes memory message = abi.encodeWithSignature("hello(address)", addr1); vm.expectRevert(ZeroAddress.selector); @@ -334,9 +482,23 @@ contract GatewayZEVMInboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors bytes memory message = abi.encodeWithSignature("hello(address)", addr1); vm.expectEmit(true, true, true, true, address(gateway)); - emit Called(owner, address(zrc20), abi.encodePacked(addr1), message, 1, revertOptions); + emit Called(owner, address(zrc20), abi.encodePacked(addr1), message, callOptions, revertOptions); gateway.call(abi.encodePacked(addr1), address(zrc20), message, 1, revertOptions); } + + function testCallWithCallOptsFailsIfReceiverIsZeroAddress() public { + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectRevert(ZeroAddress.selector); + gateway.call(abi.encodePacked(""), address(zrc20), message, callOptions, revertOptions); + } + + function testCallWithCallOpts() public { + bytes memory message = abi.encodeWithSignature("hello(address)", addr1); + vm.expectEmit(true, true, true, true, address(gateway)); + + emit Called(owner, address(zrc20), abi.encodePacked(addr1), message, callOptions, revertOptions); + gateway.call(abi.encodePacked(addr1), address(zrc20), message, callOptions, revertOptions); + } } contract GatewayZEVMOutboundTest is Test, IGatewayZEVMEvents, IGatewayZEVMErrors { diff --git a/v2/test/utils/GatewayEVMUpgradeTest.sol b/v2/test/utils/GatewayEVMUpgradeTest.sol index 83a9b8f5..6db211f6 100644 --- a/v2/test/utils/GatewayEVMUpgradeTest.sol +++ b/v2/test/utils/GatewayEVMUpgradeTest.sol @@ -75,17 +75,6 @@ contract GatewayEVMUpgradeTest is /// @param newImplementation Address of the new implementation. function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { } - /// @dev Internal function to execute a call to a destination address. - /// @param destination Address to call. - /// @param data Calldata to pass to the call. - /// @return The result of the call. - function _execute(address destination, bytes calldata data) internal returns (bytes memory) { - (bool success, bytes memory result) = destination.call{ value: msg.value }(data); - if (!success) revert ExecutionFailed(); - - return result; - } - /// @notice Pause contract. function pause() external onlyRole(PAUSER_ROLE) { _pause(); @@ -121,10 +110,12 @@ contract GatewayEVMUpgradeTest is /// @notice Executes a call to a destination address without ERC20 tokens. /// @dev This function can only be called by the TSS address and it is payable. + /// @param messageContext Message context containing sender and arbitrary call flag. /// @param destination Address to call. /// @param data Calldata to pass to the call. /// @return The result of the call. function execute( + MessageContext calldata messageContext, address destination, bytes calldata data ) @@ -136,7 +127,31 @@ contract GatewayEVMUpgradeTest is returns (bytes memory) { if (destination == address(0)) revert ZeroAddress(); - bytes memory result = _execute(destination, data); + bytes memory result; + result = _executeAuthenticatedCall(messageContext, destination, data); + + emit Executed(destination, msg.value, data); + + return result; + } + + /// @notice Executes a call to a destination address without ERC20 tokens. + /// @dev This function can only be called by the TSS address and it is payable. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function execute( + address destination, + bytes calldata data + ) + external + payable + onlyRole(TSS_ROLE) + whenNotPaused + returns (bytes memory) + { + if (destination == address(0)) revert ZeroAddress(); + bytes memory result = _executeArbitraryCall(destination, data); emit ExecutedV2(destination, msg.value, data); @@ -167,7 +182,7 @@ contract GatewayEVMUpgradeTest is if (!resetApproval(token, to)) revert ApprovalFailed(); if (!IERC20(token).approve(to, amount)) revert ApprovalFailed(); // Execute the call on the target contract - _execute(to, data); + _executeArbitraryCall(to, data); // Reset approval if (!resetApproval(token, to)) revert ApprovalFailed(); @@ -389,4 +404,46 @@ contract GatewayEVMUpgradeTest is IERC20(token).safeTransfer(custody, amount); } } + + /// @dev Internal function to execute an arbitrary call to a destination address. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function _executeArbitraryCall(address destination, bytes calldata data) internal returns (bytes memory) { + revertIfAuthenticatedCall(data); + (bool success, bytes memory result) = destination.call{ value: msg.value }(data); + if (!success) revert ExecutionFailed(); + + return result; + } + + /// @dev Internal function to execute an authenticated call to a destination address. + /// @param messageContext Message context containing sender and arbitrary call flag. + /// @param destination Address to call. + /// @param data Calldata to pass to the call. + /// @return The result of the call. + function _executeAuthenticatedCall( + MessageContext calldata messageContext, + address destination, + bytes calldata data + ) + internal + returns (bytes memory) + { + return Callable(destination).onCall(messageContext, data); + } + + // @dev prevent calling onCall function reserved for authenticated calls + function revertIfAuthenticatedCall(bytes calldata data) internal pure { + if (data.length >= 4) { + bytes4 functionSelector; + assembly { + functionSelector := calldataload(data.offset) + } + + if (functionSelector == Callable.onCall.selector) { + revert NotAllowedToCallOnCall(); + } + } + } } diff --git a/v2/test/utils/IReceiverEVM.sol b/v2/test/utils/IReceiverEVM.sol index 02d3f381..d08c7dfd 100644 --- a/v2/test/utils/IReceiverEVM.sol +++ b/v2/test/utils/IReceiverEVM.sol @@ -36,4 +36,6 @@ interface IReceiverEVMEvents { /// @param sender The address of the sender. /// @param revertContext Revert Context. event ReceivedRevert(address sender, RevertContext revertContext); + + event ReceivedOnCall(); } diff --git a/v2/test/utils/ReceiverEVM.sol b/v2/test/utils/ReceiverEVM.sol index 3d05ab2f..e8063551 100644 --- a/v2/test/utils/ReceiverEVM.sol +++ b/v2/test/utils/ReceiverEVM.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import { RevertContext } from "../../contracts/Revert.sol"; +import { MessageContext } from "../../contracts/evm/interfaces/IGatewayEVM.sol"; import "./IReceiverEVM.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -72,6 +73,10 @@ contract ReceiverEVM is IReceiverEVMEvents, ReentrancyGuard { emit ReceivedRevert(msg.sender, revertContext); } + function onCall(MessageContext calldata messageContext, bytes calldata message) external returns (bytes memory) { + emit ReceivedOnCall(); + } + /// @notice Receives ETH. receive() external payable { }