Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sender when calling connected chain contracts from zetachain #350

Closed
wants to merge 12 commits into from
66 changes: 61 additions & 5 deletions v2/contracts/evm/GatewayEVM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -71,17 +71,43 @@ 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.
/// @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 _execute(address destination, bytes calldata data) internal returns (bytes memory) {
function _executeArbitraryCall(address destination, bytes calldata data) internal returns (bytes memory) {
lumtis marked this conversation as resolved.
Show resolved Hide resolved
if (data.length >= 4) {
bytes4 functionSelector;
assembly {
functionSelector := calldataload(data.offset)
}

if (functionSelector == Callable.onCall.selector) {
revert NotAllowedToCallOnCall();
}
}
skosito marked this conversation as resolved.
Show resolved Hide resolved
(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.sender, data);
}

/// @notice Pause contract.
function pause() external onlyRole(PAUSER_ROLE) {
_pause();
Expand Down Expand Up @@ -117,10 +143,12 @@ contract GatewayEVM 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
)
Expand All @@ -132,7 +160,35 @@ contract GatewayEVM is
returns (bytes memory)
{
if (destination == address(0)) revert ZeroAddress();
bytes memory result = _execute(destination, data);
bytes memory result;
if (messageContext.isArbitraryCall) {
result = _executeArbitraryCall(destination, data);
} else {
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 Executed(destination, msg.value, data);

Expand Down Expand Up @@ -163,7 +219,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();
Expand Down
31 changes: 31 additions & 0 deletions v2/contracts/evm/interfaces/IGatewayEVM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -171,3 +189,16 @@ 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.
/// @param isArbitraryCall Indicates if call should be arbitrary or authenticated.
struct MessageContext {
address sender;
bool isArbitraryCall;
}

/// @notice Interface implemented by contracts receiving authenticated calls.
interface Callable {
function onCall(address sender, bytes calldata message) external returns (bytes memory);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe pass the entire context object to keep some consistency with the version on ZetaChain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea but currently messageContext contains sender and flag to indicate if its arb or auth call, so passing that flag here doesnt make much sense

let's see if we need to extend this context further, maybe we can define new context struct for this, waiting from @fadeev for more input on interface, but can easily be extended

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this is bad design to have this flag in messageContext, execute method that contains message context is immediately authenticated call, method without context is arbitrary call

lets then see what to put in message context beside sender and i will change it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, lets see what else to put in message context apart from sender

}
133 changes: 126 additions & 7 deletions v2/contracts/zevm/GatewayZEVM.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
);
}
Expand Down Expand Up @@ -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
);
}
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down
Loading
Loading