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:
- A frontend application where users authorize token transfers by signing a message, without needing to submit an on-chain transaction themselves.
- 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.
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 thefrom
address for the given payload, the current blockchain time is within thevalidAfter
andvalidBefore
timestamps, and the uniquenonce
has not been previously used by thefrom
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 totransferWithAuthorization
, 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 totransferWithAuthorization
).
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:
- 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"
}
]
- Official USD₮0 contract address (
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.
-
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 -
Develop your relayer script (
Relayer.ts
). This script will:- Connect to Flare using
JsonRpcProvider
, create aWallet
fromRELAYER_PRIVATE_KEY
, and instantiate the USD₮0 contract with that wallet. - Spin up an Express server with CORS and JSON parsing.
- Expose a health-check at
GET /
to confirm the service is running. - Implement
POST /relay-transfer
, which:- Destructures
{ payload, v, r, s }
from the request body. - Calls
usd0.transferWithAuthorization(...)
, passing the six payload fields plusv r s
, and sets an explicitgasLimit
of120_000
. - Waits for the transaction to mine, then returns
{ txHash }
; on error it returns{ error }
.
- Destructures
- Start listening on
PORT
(default3000
) and log a success message.
Here's an example implementation:
Relayer.tsimport 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}`);
}); - Connect to Flare using
-
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.
-
In your frontend project's root directory, create a
.env
file. Vite exposes these variables to your app viaimport.meta.env
:VITE_USD0_ADDRESS=0xe7cd86e13AC4309349F30B3435a9d337750fC82D # the onchain USD₮0 token contract
VITE_RELAYER_URL=http://localhost:3000 # your relayer endpoint -
Develop the frontend component (
App.tsx
). The main application component will handle:- Connect the wallet (
window.ethereum
) and request an account. - Instantiate an
ethers.BrowserProvider
andSigner
. - Fetch the token's EIP-712 domain (name, version, chainId, contract).
- Define the
TransferWithAuthorization
typed-data fields. - 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. - Call
signer.signTypedData(domain, types, message)
to pop the wallet and produce a signature. - Extract
(v, r, s)
from that signature. - POST the JSON payload plus
(v, r, s)
to your relayer at${VITE_RELAYER_URL}/relay-transfer
.
Here's an example implementation:
App.tsximport { 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 USD₮0 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>
);
} - Connect the wallet (
-
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 devVite will typically start the app and open it in your browser.
Run the app
- 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. - Start the Frontend: Open another terminal, navigate to your frontend's project directory, and run
npm run dev
. - Interact with the frontend:
- Open the frontend application in your web browser and connect your EVM wallet (ensure it's set to Flare Mainnet).
- Enter a recipient address and the amount of USD₮0 to transfer.
- Click Send Gasless. Your wallet will prompt you for a signature (this is offchain and gas-free for you).
- Once signed, the frontend sends the authorization to the relayer, which then submits the actual transaction to Flare Mainnet, paying the gas fee.
- 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.