Skip to main content
Developers
Cross-Chain Messaging
Gas Fees

Cross-Chain Messaging Fees

A user (wallet, contract) must pay fees in order to send data and value across chains through ZetaChain. A user pays for fees by sending ZETA (and message data) on a connected chain to a Connector contract. This ZETA is used to pay validators/stakers/ecosystem pools, as well as for paying the gas on the destination. To a user, this is all bundled into a single transaction.

When sending a cross-chain message you're paying two types of fees:

  • Outbound gas fee: calculated dynamically based on the gas prices for the destination chain, the gas limit supplied by the user and the token prices on the liquidity pools on ZetaChain.
  • Protocol fee: currently, a fixed value defined in the ZetaChain source code.

Current Cross-Chain Messaging Fees

In the table below you can see the current fees. The fees are defined in ZETA tokens and are calculated for the destination chain (the chain where the message is sent to). The fees are calculated with the gas limit of 500000.

Chain IDTotal FeeGas FeeProtocol Fee

To calculate fees for a different gas limit, please, check out the fees command in the smart-contract template:

npx hardhat fees

Different Approaches to Paying the Fees

When you write a smart contract that uses cross chain messages this contract needs to pay fees in ZETA for every cross chain transaction. There are several ways to handle this.

Sending ZETA to the Connector

In your cross-chain messaging contract approve zetaValueAndGas amount of ZETA tokens to the connector and then transfer them to the connector contract.

The main disadvantage with this approach is that the user must approve your contract before and they have to have enough ZETA in his wallet.

function sendMessage(uint256 destinationChainId, bytes calldata destinationAddress, uint256 zetaValueAndGas) external {
if (zetaValueAndGas == 0) revert InvalidZetaValueAndGas();

bool success1 = ZetaEth(zetaToken).approve(address(connector), zetaValueAndGas);
bool success2 = ZetaEth(zetaToken).transferFrom(msg.sender, address(this), zetaValueAndGas);
if (!(success1 && success2)) revert ErrorTransferringZeta();

connector.send(
ZetaInterfaces.SendInput({
destinationChainId: destinationChainId,
destinationAddress: destinationAddress,
destinationGasLimit: 300000,
message: abi.encode(),
zetaValueAndGas: zetaValueAndGas,
zetaParams: abi.encode("")
})
);
}

Pay With ZETA From the Contract

You can add ZETA tokens to the contract and the contract will use these tokens when sending cross chain messages.

This is easier for end-users, because they don't have to think about using ZETA tokens, but it's more complex for the contract developer because they have to ensure that the contract has enough ZETA tokens.

function sendMessage(uint256 destinationChainId, bytes calldata destinationAddress) external {
bool success1 = ZetaEth(zetaToken).approve(address(connector), ZETA_GAS);
if (!success1) revert ErrorApprovingZeta();

connector.send(
ZetaInterfaces.SendInput({
destinationChainId: destinationChainId,
destinationAddress: destinationAddress,
destinationGasLimit: 300000,
message: abi.encode(),
zetaValueAndGas: ZETA_GAS,
zetaParams: abi.encode("")
})
);
}

Pay With Any Token and Swap to ZETA

Your contract can accept any token and swap it to ZETA internally.

This approach is more complex, because you need to add a swap logic to your contract and take market price fluctuations into account. But it's more convenient for end-users, because they can use any token to pay for cross chain messages without even knowing that ZETA is being used under the hood.

To make it eaier you can use ZetaConsumer's getZetaFromEth to swap any token to ZETA.

function sendMessage(uint256 destinationChainId, bytes calldata destinationAddress) external payable{
uint256 crossChainGas = 2 * (10 ** 18);
uint256 zetaValueAndGas = _zetaConsumer.getZetaFromEth{value: msg.value}(address(this), crossChainGas);
bool success1 = ZetaEth(zetaToken).approve(address(connector), zetaValueAndGas);
if (!success1) revert ErrorApprovingZeta();

connector.send(
ZetaInterfaces.SendInput({
destinationChainId: destinationChainId,
destinationAddress: destinationAddress,
destinationGasLimit: 300000,
message: abi.encode(),
zetaValueAndGas: zetaValueAndGas,
zetaParams: abi.encode("")
})
);
}

ZetaTokenConsumer is an interface with several implementations that handles all the logic you need to swap ZETA from/to another token. Right now we have three implementations (Uniswap V2, Uniswap V3, and Trident) using different DEX. You can include it in your contract and just call the appropriate method.