Skip to main content

Gasless USD₮0 Transfers

Flare's USD₮0 integration enables native, gasless USDT transfers (also known as meta-transactions) on the Flare network, allowing your end-users to avoid paying gas fees directly. In this guide, you will build a system for gasless USD₮0 transfers:

  1. A frontend application where users authorize token transfers by signing a message, without needing to submit an on-chain transaction themselves.
  2. A backend relayer service that takes this signed authorization and submits the actual transaction to the Flare network, covering the gas fees on the user's behalf.

This powerful pattern significantly enhances user experience by abstracting away the complexities and costs of network gas fees.

Beyond USD₮0

This guide uses USD₮0 as the primary example, but the underlying principles and a similar implementation logic can be applied to other ERC-20 tokens on Flare that support EIP-3009.

Meta-transactions

Meta-transactions separate the authorization of an action (by the user) from its execution (by a third-party relayer). This is key to enabling gasless experiences. Two Ethereum Improvement Proposals (EIPs) are central to this implementation:

EIP-712: Typed Structured Data Hashing and Signing

EIP-712 standardizes the way structured data is hashed and signed. Instead of signing an obscure hexadecimal string, users are presented with a human-readable message in their wallets, detailing what they are authorizing. This ensures transparent offchain signing.

EIP-3009: Transfer with Authorization

EIP-3009 extends the ERC-20 token standard to include support for meta-transactions. It allows a token holder to sign an authorization message offchain, which can then be relayed by another account (the relayer) to execute the transfer on-chain. The relayer pays the gas fees for this on-chain execution.

EIP-3009 introduces two key functions, including:

  • transferWithAuthorization: This function moves tokens from the from address (the authorizer) to the to address. It only executes if the provided signature (v, r, s) correctly matches the from address for the given payload, the current blockchain time is within the validAfter and validBefore timestamps, and the unique nonce has not been previously used by the from address for this contract.

    transferWithAuthorization(
    address from, // Payer's address (Authorizer)
    address to, // Payee's address
    uint256 value, // Amount to be transferred
    uint256 validAfter, // The time after which this is valid (unix time)
    uint256 validBefore, // The time before which this is valid (unix time)
    uint256 nonce, // Unique nonce
    uint8 v, // v of the signature
    bytes32 r, // r of the signature
    bytes32 s // s of the signature
    ) external;
  • receiveWithAuthorization: Similar to transferWithAuthorization, this function allows a designated party (often the recipient or a relayer) to "pull" tokens from the authorizer's account. This can be useful for scenarios like collecting fees upon receipt of services. (The implementation details are analogous to transferWithAuthorization).

Both functions incorporate timestamps (validAfter, validBefore) to prevent stale authorizations from being executed indefinitely and a nonce (a number used once) to protect against replay attacks, ensuring a signed message can only be submitted once.

Prerequisites

Before you begin, ensure you have the following:

  • An EVM compatible wallet (e.g., Metamask). You can find suitable options on the Flare Wallets page.
  • A Relayer Account: An EOA on Flare Mainnet, funded with sufficient FLR to cover the gas costs of relaying transactions.
  • Development Environment:
    • Node.js (v18 or later)
    • A package manager (npm or yarn)
    • A React frontend setup, preferably using Vite for quick project scaffolding.
  • USD₮0 Contract Details:
    • Official USD₮0 contract address (TetherTokenOFTExtension) for Flare Mainnet. Always refer to the official USD₮0 documentation for the latest addresses.
    • The ABI for the USD₮0 contract. For this guide, we only need the name andtransferWithAuthorization functions.
      Relevant portion of USD₮0 contract ABI
      USD0.json
      [
      {
      "inputs": [],
      "name": "name",
      "outputs": [
      {
      "internalType": "string",
      "name": "",
      "type": "string"
      }
      ],
      "stateMutability": "view",
      "type": "function"
      },
      {
      "inputs": [
      {
      "internalType": "address",
      "name": "from",
      "type": "address"
      },
      {
      "internalType": "address",
      "name": "to",
      "type": "address"
      },
      {
      "internalType": "uint256",
      "name": "value",
      "type": "uint256"
      },
      {
      "internalType": "uint256",
      "name": "validAfter",
      "type": "uint256"
      },
      {
      "internalType": "uint256",
      "name": "validBefore",
      "type": "uint256"
      },
      {
      "internalType": "bytes32",
      "name": "nonce",
      "type": "bytes32"
      },
      {
      "internalType": "bytes",
      "name": "signature",
      "type": "bytes"
      }
      ],
      "name": "transferWithAuthorization",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
      }
      ]

Implementation

Now, let's build the two main components of our gasless transfer system: the backend relayer and the frontend application.

Build the Relayer service

The relayer is a Node.js Express service responsible for submitting the user's signed authorization to the blockchain.

  1. Create a .env file in your relayer project's root directory. These variables configure the relayer's connection to Flare Mainnet and its operational parameters.

    FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc # RPC for Flare Mainnet
    USD0_ADDRESS=0xe7cd86e13AC4309349F30B3435a9d337750fC82D # USD₮0 token contract
    RELAYER_PRIVATE_KEY=0x...abc # Relayer's private key funded with FLR
    PORT=3000 # Port to listen on
  2. Develop your relayer script (Relayer.ts). This script will:

    1. Connect to Flare using JsonRpcProvider, create a Wallet from RELAYER_PRIVATE_KEY, and instantiate the USD₮0 contract with that wallet.
    2. Spin up an Express server with CORS and JSON parsing.
    3. Expose a health-check at GET / to confirm the service is running.
    4. Implement POST /relay-transfer, which:
      • Destructures { payload, v, r, s } from the request body.
      • Calls usd0.transferWithAuthorization(...), passing the six payload fields plus v r s, and sets an explicit gasLimit of 120_000.
      • Waits for the transaction to mine, then returns { txHash }; on error it returns { error }.
    5. Start listening on PORT (default 3000) and log a success message.

    Here's an example implementation:

    Relayer.ts
    import express from "express";
    import cors from "cors";
    import { JsonRpcProvider, Wallet, Contract } from "ethers";
    import USD0Abi from "./USD0.json";

    import "dotenv/config";

    // 1) Load and validate environment variables
    const {
    FLARE_RPC_URL,
    USD0_ADDRESS,
    RELAYER_PRIVATE_KEY,
    PORT = "3000", // Default port
    RELAYER_GAS_LIMIT = "120000", // Default gas limit, configurable
    } = process.env;

    if (!FLARE_RPC_URL || !USD0_ADDRESS || !RELAYER_PRIVATE_KEY) {
    console.error(
    "❌ Critical environment variable missing: Ensure FLARE_RPC_URL, USD0_ADDRESS, and RELAYER_PRIVATE_KEY are set.",
    );
    process.exit(1); // Exit if critical configs are missing
    }

    // 2) Set up ethers.js provider, wallet, and contract instance
    const provider = new JsonRpcProvider(FLARE_RPC_URL);
    const relayerWallet = new Wallet(RELAYER_PRIVATE_KEY, provider);
    const usd0 = new Contract(USD0_ADDRESS, USD0Abi, relayerWallet);

    // 3) Create and configure the Express application
    const app = express();
    app.use(cors()); // Enable Cross-Origin Resource Sharing
    app.use(express.json()); // Middleware to parse JSON request bodies

    // 4) Health-check endpoint
    app.get("/", (_req, res) => {
    res.send(
    `✅ Gasless relayer is operational. Relayer account: ${relayerWallet.address}`,
    );
    });

    // 5) Gasless transfer endpoint
    app.post("/relay-transfer", async (req, res) => {
    try {
    const { payload, v, r, s } = req.body;
    console.log(
    `[${new Date().toISOString()}] Received relay request: from=${payload.from}, to=${payload.to}, value=${payload.value}`,
    );

    const tx = await usd0.transferWithAuthorization(
    payload.from,
    payload.to,
    payload.value,
    payload.validAfter,
    payload.validBefore,
    payload.nonce,
    v,
    r,
    s,
    { gasLimit: Number(RELAYER_GAS_LIMIT) }, // Use configurable gas limit
    );

    console.log(
    `Transaction submitted with hash: ${tx.hash}. Waiting for confirmation...`,
    );
    const receipt = await tx.wait(); // Waits for 1 confirmation by default

    console.log(
    `Transaction ${tx.hash} confirmed in block ${receipt?.blockNumber}`,
    );
    res.json({ txHash: tx.hash, blockNumber: receipt?.blockNumber });

    await tx.wait();
    res.json({ txHash: tx.hash });
    } catch (err: unknown) {
    console.error(
    `[${new Date().toISOString()}] Relayer error processing request:`,
    err,
    );
    res.status(500).json({ error: err.message });
    }
    });

    // 6) Start the Express server
    const portNumber = Number(PORT);
    app.listen(portNumber, () => {
    console.log(`✅ Relayer service listening on http://localhost:${portNumber}`);
    console.log(`🔑 Relayer wallet address: ${relayerWallet.address}`);
    console.log(`⛽ Default Gas Limit for transactions: ${RELAYER_GAS_LIMIT}`);
    });
  3. Install dependencies and run the Relayer:

    # Install dependencies (example)
    # npm install express ethers cors dotenv
    # npm install -D typescript tsx @types/express @types/cors

    # Run the relayer
    npx tsx Relayer.ts

You should see a log message indicating the relayer is listening on the specified port. This service must be running for the frontend to successfully relay meta-transactions.

Build the frontend

The frontend allows users to authorize transfers without directly paying gas. It uses React and Vite.

  1. In your frontend project's root directory, create a .env file. Vite exposes these variables to your app via import.meta.env:

    VITE_USD0_ADDRESS=0xe7cd86e13AC4309349F30B3435a9d337750fC82D # the onchain USD₮0 token contract
    VITE_RELAYER_URL=http://localhost:3000 # your relayer endpoint
  2. Develop the frontend component (App.tsx). The main application component will handle:

    1. Connect the wallet (window.ethereum) and request an account.
    2. Instantiate an ethers.BrowserProvider and Signer.
    3. Fetch the token's EIP-712 domain (name, version, chainId, contract).
    4. Define the TransferWithAuthorization typed-data fields.
    5. Build the payload object - serializing value to a string so it JSON-encodes correctly, and setting a one-hour validity window plus a fresh 32-byte nonce.
    6. Call signer.signTypedData(domain, types, message) to pop the wallet and produce a signature.
    7. Extract (v, r, s) from that signature.
    8. POST the JSON payload plus (v, r, s) to your relayer at ${VITE_RELAYER_URL}/relay-transfer.

    Here's an example implementation:

    App.tsx
    import { useState } from "react";
    import { ethers, Eip1193Provider } from "ethers";
    import USD0Abi from "./USD0.json";

    // Environment variables
    const USD0_ADDRESS = import.meta.env.VITE_USD0_ADDRESS!;
    const RELAYER_URL = import.meta.env.VITE_RELAYER_URL!;

    // Constants
    const EIP712_DOMAIN_VERSION = "1";
    const SIGNATURE_VALIDITY_PERIOD_SECONDS = 3600; // 1 hour
    const USD0_DECIMALS = 6;

    declare global {
    interface Window {
    ethereum?: Eip1193Provider;
    }
    }

    export default function App() {
    const [to, setTo] = useState("");
    const [amount, setAmount] = useState("1");

    async function sendGasless() {
    if (!window.ethereum) {
    alert("Please install MetaMask or a compatible Ethereum wallet.");
    return;
    }

    // 1) Provider & Signer
    const provider = new ethers.BrowserProvider(window.ethereum);
    await provider.send("eth_requestAccounts", []);
    const signer = await provider.getSigner();

    // 2) EIP-712 Domain Data (Fetched in parallel)
    const tokenContract = new ethers.Contract(USD0_ADDRESS, USD0Abi, provider);
    const [signerAddress, network, tokenName] = await Promise.all([
    signer.getAddress(),
    provider.getNetwork(),
    tokenContract.name() as Promise<string>, // Explicitly type if ABI isn't fully typed
    ]);

    const domain = {
    name: tokenName,
    version: EIP712_DOMAIN_VERSION,
    chainId: network.chainId,
    verifyingContract: USD0_ADDRESS,
    };

    const types = {
    TransferWithAuthorization: [
    { name: "from", type: "address" },
    { name: "to", type: "address" },
    { name: "value", type: "uint256" },
    { name: "validAfter", type: "uint256" },
    { name: "validBefore", type: "uint256" },
    { name: "nonce", type: "bytes32" },
    ],
    };

    // 3) Build the payload message
    const nowSeconds = Math.floor(Date.now() / 1000);
    const message = {
    from: signerAddress,
    to,
    value: ethers.parseUnits(amount, USD0_DECIMALS).toString(),
    validAfter: nowSeconds,
    validBefore: nowSeconds + SIGNATURE_VALIDITY_PERIOD_SECONDS,
    nonce: ethers.hexlify(ethers.randomBytes(32)),
    };

    // 4) Sign Typed Data
    const signature = await signer.signTypedData(domain, types, message);
    const { v, r, s } = ethers.Signature.from(signature);

    // 5) POST to Relayer
    const response = await fetch(`${RELAYER_URL}/relay-transfer`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ payload: message, v, r, s }),
    });

    if (!response.ok) {
    const errorData = await response.json().catch(() => ({
    error: "Relayer request failed with status: " + response.status,
    }));
    throw new Error(errorData.error || "Relayer request failed.");
    }
    const { txHash } = await response.json();
    alert("✅ Sent! On-chain tx hash:\n" + txHash);
    }

    return (
    <div style={{ padding: 20, maxWidth: 400 }}>
    <h1>Gasless USD0 Demo</h1>
    <input
    placeholder="Recipient address"
    value={to}
    onChange={(e) => setTo(e.target.value)}
    style={{ width: "100%", marginBottom: 8 }}
    />
    <input
    placeholder="Amount (e.g. 0.5)"
    value={amount}
    onChange={(e) => setAmount(e.target.value)}
    style={{ width: "100%", marginBottom: 12 }}
    />
    <button onClick={sendGasless}>Send Gasless</button>
    </div>
    );
    }
  3. Install dependencies and run the frontend:

    # Install dependencies (example, if starting a new Vite + React + TS project)
    # npm create vite@latest my-gasless-app -- --template react-ts
    # cd my-gasless-app
    # npm install ethers

    # Run the frontend development server
    npm run dev

    Vite will typically start the app and open it in your browser.

Run the app

  1. Start the Relayer Service: Navigate to your relayer's project directory in a terminal and run npx tsx Relayer.ts (or your configured start script). Confirm it's listening.
  2. Start the Frontend: Open another terminal, navigate to your frontend's project directory, and run npm run dev.
  3. Interact with the frontend:
    1. Open the frontend application in your web browser and connect your EVM wallet (ensure it's set to Flare Mainnet).
    2. Enter a recipient address and the amount of USD₮0 to transfer.
    3. Click Send Gasless. Your wallet will prompt you for a signature (this is offchain and gas-free for you).
    4. Once signed, the frontend sends the authorization to the relayer, which then submits the actual transaction to Flare Mainnet, paying the gas fee.
    5. Observe the feedback from the application for transaction status, and check the transaction on Flarescan.

Congratulations! You've now implemented a foundational system for gasless USD₮0 transfers on Flare Mainnet. This approach leverages EIP-712 and EIP-3009 to create a significantly improved user experience by abstracting away gas fees.

Further enhancements

For a production-ready application, consider these further enhancements:

  • Robust Error Handling: Implement comprehensive error handling and user-friendly feedback mechanisms in both the frontend and relayer.
  • Input Validation & Security: Add thorough validation for all inputs to the relayer to prevent abuse and ensure data integrity. Implement security best practices (e.g., rate limiting, authentication if needed).
  • Dynamic Gas Strategy: Instead of a fixed gas limit, the relayer could dynamically estimate gas prices and limits for transactions to optimize costs and improve reliability.
  • Transaction Monitoring: Provide users with clear status updates on their relayed transactions.
  • Nonce Management: While EIP-3009 handles nonce checking at the contract level, your relayer might benefit from its own nonce tracking or management for specific users if it needs to handle multiple pending transactions for the same user.