Skip to main content
Meta-transactions allow users to interact with smart contracts without paying gas fees. A relayer (like Gelato) pays the gas fees and executes the transaction on behalf of the user through:
  1. EIP-712 Typed Data Signing: Users sign structured data instead of raw transactions
  2. Contract Inheritance: Contracts inherit meta-transaction functionality
  3. Signature Verification: Contracts verify user signatures and execute functions on their behalf

Contract Conversion

Original Contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;

contract SimpleCounter {
    uint256 public counter;

    event IncrementCounter(address msgSender, uint256 newCounterValue, uint256 timestamp);

    function increment() external {
        counter++;
        emit IncrementCounter(msg.sender, counter, block.timestamp);
    }
}

Meta-Transaction Enabled Contract

To enable meta-transactions:
  1. Inherit from EIP712MetaTransaction
  2. Replace msg.sender with msgSender()
// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;

import "./EIP712MetaTransaction.sol";

contract SimpleCounter is EIP712MetaTransaction("SimpleCounter", "1") {
    uint256 public counter;

    event IncrementCounter(address msgSender, uint256 newCounterValue, uint256 timestamp);

    function increment() external {
        counter++;
        emit IncrementCounter(msgSender(), counter, block.timestamp);
    }
}

Key Changes

OriginalMeta-Transaction Enabled
contract SimpleCountercontract SimpleCounter is EIP712MetaTransaction("SimpleCounter", "1")
msg.sendermsgSender()
  • Inheritance: Passes contract name and version for EIP-712 domain separation
  • msgSender(): Returns the original user address (not the relayer address)

What EIP712MetaTransaction Provides

  • executeMetaTransaction(): Main function to execute meta-transactions
  • getNonce(address user): Get user’s nonce for replay protection
  • msgSender(): Returns the original user address
  • EIP-712 domain separation to prevent signature collisions

Client Implementation

Setup EIP-712 Types and Domain

import { pad, toHex, type Hex } from 'viem';

const types = {
  MetaTransaction: [
    { name: 'nonce', type: 'uint256' },
    { name: 'from', type: 'address' },
    { name: 'functionSignature', type: 'bytes' },
  ],
} as const;

const domain = {
  name: 'SimpleCounter',
  version: '1',
  verifyingContract: simpleCounterAddress as Hex,
  salt: pad(toHex(chainId), { size: 32 }),
};

Complete Example

import { createGelatoEvmRelayerClient, StatusCode, sponsored } from '@gelatocloud/gasless';
import {
  createPublicClient,
  createWalletClient,
  http,
  encodeFunctionData,
  pad,
  toHex,
  type Hex
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const simpleCounterAddress = '0x5115B85246bb32dCEd920dc6a33E2Be6E37fFf6F';

const simpleCounterAbi = [
  { name: 'increment', type: 'function', inputs: [], outputs: [] },
  { name: 'counter', type: 'function', inputs: [], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
  { name: 'getNonce', type: 'function', inputs: [{ name: 'user', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
  { name: 'executeMetaTransaction', type: 'function', inputs: [
    { name: 'userAddress', type: 'address' },
    { name: 'functionSignature', type: 'bytes' },
    { name: 'sigR', type: 'bytes32' },
    { name: 'sigS', type: 'bytes32' },
    { name: 'sigV', type: 'uint8' }
  ], outputs: [{ type: 'bytes' }] }
] as const;

const metaTransactionTypes = {
  MetaTransaction: [
    { name: 'nonce', type: 'uint256' },
    { name: 'from', type: 'address' },
    { name: 'functionSignature', type: 'bytes' },
  ],
} as const;

const executeMetaTransaction = async () => {
  const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);

  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(),
  });

  const walletClient = createWalletClient({
    account,
    chain: baseSepolia,
    transport: http(),
  });

  const relayer = createGelatoEvmRelayerClient({
    apiKey: process.env.GELATO_API_KEY,
    testnet: true,
  });

  // Get user nonce from contract
  const nonce = await publicClient.readContract({
    address: simpleCounterAddress,
    abi: simpleCounterAbi,
    functionName: 'getNonce',
    args: [account.address],
  });

  // Encode the increment() function call
  const functionSignature = encodeFunctionData({
    abi: simpleCounterAbi,
    functionName: 'increment',
  });

  // Setup EIP-712 domain (salt is chain ID padded to 32 bytes)
  const domain = {
    name: 'SimpleCounter',
    version: '1',
    verifyingContract: simpleCounterAddress as Hex,
    salt: pad(toHex(baseSepolia.id), { size: 32 }),
  };

  // Sign the meta-transaction using EIP-712
  const signature = await walletClient.signTypedData({
    domain,
    types: metaTransactionTypes,
    primaryType: 'MetaTransaction',
    message: {
      nonce: nonce,
      from: account.address,
      functionSignature: functionSignature,
    },
  });

  // Parse signature into r, s, v components
  const r = `0x${signature.slice(2, 66)}` as Hex;
  const s = `0x${signature.slice(66, 130)}` as Hex;
  const v = parseInt(signature.slice(130, 132), 16);

  // Encode executeMetaTransaction call
  const metaPayload = encodeFunctionData({
    abi: simpleCounterAbi,
    functionName: 'executeMetaTransaction',
    args: [account.address, functionSignature, r, s, v],
  });

  // Send via Gelato Relay
  const id = await relayer.sendTransaction({
    chainId: baseSepolia.id,
    to: simpleCounterAddress,
    data: metaPayload,
    payment: sponsored(),
  });

  console.log(`Gelato task ID: ${id}`);

  // Wait for status
  const status = await relayer.waitForStatus({ id });

  if (status.status === StatusCode.Included) {
    console.log(`Transaction hash: ${status.receipt.transactionHash}`);
  } else {
    console.log(`Transaction failed: ${status.message}`);
  }
};

executeMetaTransaction();

EIP712MetaTransaction.sol

Create this base contract in your project to inherit meta-transaction functionality:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.29;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract EIP712MetaTransaction {
    using ECDSA for bytes32;

    bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256(
        bytes("EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)")
    );

    bytes32 internal constant META_TRANSACTION_TYPEHASH = keccak256(
        bytes("MetaTransaction(uint256 nonce,address from,bytes functionSignature)")
    );

    bytes32 internal domainSeparator;
    mapping(address => uint256) private nonces;

    struct MetaTransaction {
        uint256 nonce;
        address from;
        bytes functionSignature;
    }

    constructor(string memory name, string memory version) {
        domainSeparator = keccak256(
            abi.encode(
                EIP712_DOMAIN_TYPEHASH,
                keccak256(bytes(name)),
                keccak256(bytes(version)),
                address(this),
                bytes32(block.chainid)
            )
        );
    }

    function executeMetaTransaction(
        address userAddress,
        bytes memory functionSignature,
        bytes32 sigR,
        bytes32 sigS,
        uint8 sigV
    ) public payable returns (bytes memory) {
        MetaTransaction memory metaTx = MetaTransaction({
            nonce: nonces[userAddress],
            from: userAddress,
            functionSignature: functionSignature
        });

        require(verify(userAddress, metaTx, sigR, sigS, sigV), "Invalid signature");
        nonces[userAddress]++;

        (bool success, bytes memory returnData) = address(this).call(
            abi.encodePacked(functionSignature, userAddress)
        );
        require(success, "Function call failed");

        return returnData;
    }

    function getNonce(address user) external view returns (uint256) {
        return nonces[user];
    }

    function verify(
        address user,
        MetaTransaction memory metaTx,
        bytes32 sigR,
        bytes32 sigS,
        uint8 sigV
    ) internal view returns (bool) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                domainSeparator,
                keccak256(
                    abi.encode(
                        META_TRANSACTION_TYPEHASH,
                        metaTx.nonce,
                        metaTx.from,
                        keccak256(metaTx.functionSignature)
                    )
                )
            )
        );

        address recovered = digest.recover(sigV, sigR, sigS);
        return recovered == user && recovered != address(0);
    }

    function msgSender() internal view returns (address sender) {
        if (msg.sender == address(this)) {
            bytes memory array = msg.data;
            uint256 index = msg.data.length;
            assembly {
                sender := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff)
            }
        } else {
            sender = msg.sender;
        }
        return sender;
    }
}
This contract uses OpenZeppelin’s ECDSA library for signature recovery. Install it with:
npm install @openzeppelin/contracts