Cross-chain gas abstraction with Axelar


Interoperability is one of the biggest pain points in blockchain today. Users need to manage wallets and gas tokens on every chain they touch, creating friction and limiting adoption.

For example, if someone receives their salary on a chain they don't normally use, they must bridge in that chain's native token to be able to pay for the gas just to move out those newly received funds. It makes simple collaboration on the blockchain unnecessarily difficult.

Current solutions require manual bridging and swapping, which is complex for the average user and risky when it comes to security.

The good news is that Axelar introduces gas abstraction, allowing users to pay gas for transactions on one chain using tokens on another, removing the need to hold native gas tokens on every chain they interact with.

System Design

To overcome the complexity of managing native gas tokens across multiple blockchains, we will build a cross-chain gas payment system that allows users to initiate a transaction on Chain A (Ethereum) without needing any native tokens or funds there. Instead, the user pays gas using USDC held on Chain B (Polygon).

We implement this using Axelar's interoperability stack, specifically the General Messaging Protocol (GMP) for contract execution across chains, the Interchain Token Service (ITS) to burn/mint USDC tokens implemented natively across chains and the Gas Service to prepay for the cross-chain expenses using ERC-20s.

Here's how it works:

Flow Overview

  1. User triggers a contract on Polygon.
  2. USDC is sent and gas fees are paid on Polygon with the GasService, USDC is burned.
  3. The GMP routes the request to Ethereum.
  4. The ITS mints equivalent USDC on Ethereum.
  5. The final transfer is executed with prepaid gas.

System Components

  • User wallet on Chain B
  • CrossChainGasAbstraction contract deployed on both chains
  • Axelar GMP
  • Axelar ITS
  • User wallet on Chain A

Cross-chain gas abstraction with Axelar

GMP & ITS

General Message Passing

Axelar's General Message Passing enables secure and programmable communication between smart contracts on different chains. It's at the core of the cross-chain gas abstraction system by allowing both data and token transfers to be executed across chains reliably.

In this system, GMP is used to relay a function call from the source chain to the destination chain, along with a token payload and the corresponding gas payment.

  1. The CrossChainGasAbstraction smart contract inherits from AxelarExecutableWithToken. This gives it the ability to receive and act on cross-chain messages that include tokens.
  2. On the source chain or Chain B, the user invokes the sendUSDC() function, which:
    • Approves and transfers USDC to the Axelar Gateway.
    • Calls payGasForContractCallWithToken() from the Axelar GasService.
    • Calls callContractWithToken() to submit a message payload and USDC tokens to the Axelar Gateway.
  3. payGasForContractCallWithToken() tells Axelar's relayers: "Here's the gas to cover the contract execution on the destination chain." Without this, the destination contract would not execute automatically.
  4. Axelar's off-chain relayers pick up the approved and paid-for message. They perform consensus on the source transaction and forward it to the destination chain or Chain A.
  5. On Ethereum, Axelar's Gateway contract invokes _executeWithToken() in the contract, passing along the token amount.

Interchain Token Service

Axelar's Interchain Token Service powers token transfers across chains without the use of wrapped assets like before, which typically add complexity and risk. It's essential to the gas abstraction system because it ensures that a single token can be natively recognized and used on multiple chains.

  1. Tokens like USDC must be registered with Axelar's Interchain Token Service. Once registered, they become interoperable, meaning that they can be burned on one chain and minted on another, maintaining a consistent total supply across chains.
  2. When a user sends USDC from Polygon to Ethereum:
    • The Interchain Token Service burns USDC on Polygon.
    • It then mints the equivalent amount of USDC on Ethereum.
  3. Because the user now holds USDC natively on Ethereum, that token can be consumed by the Gas Service to cover gas fees on Ethereum, even if the user holds no ETH.

Gas Fee Estimation

One of the challenges of this implementation is predicting how much gas to pay for a cross-chain message. Prices vary per chain, and relayer costs are dynamic which makes gas estimations very tricky.

This contract snippet demonstrates how to estimate the gas cost of a cross-chain message using Axelar's built-in gas estimator. It uses the actual IAxelarGasService.estimateGasFee interface to return a reliable estimate.

function estimateGasFee(
    string calldata destinationChain,
    string calldata destinationAddress,
    string calldata _message
) external view returns (uint256) {
    return gasService.estimateGasFee(
        destinationChain,
        destinationAddress,
        payload,
        GAS_LIMIT,
        new bytes(0)
    );
}

Smart Contract Example

Below is a minimal version of the CrossChainGasAbstraction .

/**
 * @notice Enables cross-chain USDC transfers and executes logic on the 
    destination chain using Axelar GMP.
 * Users pay destination chain gas fees upfront in ERC-20 tokens via 
    Axelar Gas Service.
 * @dev Assumes USDC is already registered with Axelar's Interchain Token Servi      e (ITS) across all involved chains.
 */
contract CrossChainGasAbstraction is AxelarExecutableWithToken {

 IAxelarGasService public immutable gasService;

 event ExecutedWithToken( bytes32 commandId, string sourceChain,
 string sourceAddress, bytes payload, string tokenSymbol, uint256 amount);

  /**
   * @param _gateway Address of the Axelar Gateway sc on the deployed chain
   * @param _gasReceiver Address of the GasService sc on the deployed chain
   */
  constructor(address _gateway, address _gasReceiver) 
  AxelarExecutableWithToken(_gateway){
      gasService = IAxelarGasService(_gasReceiver);
  }

 /**
 * @notice The transaction sent triggered from the src chain
 * @dev destinationAddress Will be passed in as gmp message in this tx
 * @param destinationChain Name of the dest chain
 * @param destinationAddress Address on dest chain this tx is going to
 * @param destinationAddressForUSDC Recipient addresses receiving sent funds
 * @param symbol Symbol of supported token being sent
 * @param amount Amount of tokens being sent
 * @param gasToken Amount of tokens to pay for the gas
 * @param gasFeeAmount Amount of tokens to pay for the gas
*/
function sendUSDC(
    string calldata destinationChain,
    string calldata destinationAddress,
    address destinationAddressForUSDC,
    string calldata symbol,
    uint256 amount,
    address gasToken,
    uint256 gasFeeAmount 
) external payable {
    require(msg.value > 0, 'Gas payment is required');

    address tokenAddress = gatewayWithToken().tokenAddresses(symbol);
    IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount);
    IERC20(tokenAddress).approve(address(gatewayWithToken()), amount);
    bytes memory payload = abi.encode(destinationAddressForUSDC);
    
    gasService.payGasForContractCallWithToken(
        address(this),
        destinationChain, 
        destinationAddress, 
        payload, 
        symbol, 
        amount, 
        gasToken, 
        gasFeeAmount, 
        msg.sender 
    );
    gatewayWithToken().callContractWithToken(destinationChain, 
    destinationAddress, payload, symbol, amount);
}

/**
 * @notice Called automatically by Axelar's Relayer on the destination chain
 * @dev Decodes the recipient address and transfers the received tokens.
 * @param payload Encoded address of the final recipient
 * @param tokenSymbol Symbol of supported token sent from src chain
 * @param amount Amount of tokens sent from src chain
 */
  function _executeWithToken(
      bytes32 commandId,
      string calldata sourceChain,
      string calldata sourceAddress,
      bytes calldata payload,
      string calldata tokenSymbol,
      uint256 amount
  ) internal override {
      address recipient = abi.decode(payload, (address));
       // Resolve the token address from Axelar's registry
      address tokenAddress = gatewayWithToken().tokenAddresses(tokenSymbol);
      // Transfer the received token to the recipient
      IERC20(tokenAddress).transfer(recipient, amount);

      emit ExecutedWithToken(commandId, sourceChain, sourceAddress, payload,
       tokenSymbol, amount);
  }
}