Gas-Free Transactions: Implementing Meta-Transactions in Solidity
Learn how to implement EIP-2771 meta-transactions so users can interact with your contracts without holding ETH for gas.
The Gas UX Problem and Meta-Transactions
Requiring users to hold ETH before they can interact with your dApp is one of the biggest onboarding barriers in Web3. Meta-transactions solve this by separating the signer (the user) from the gas payer (a relayer). The user signs a message off-chain; the relayer submits the actual on-chain transaction and pays gas.
Two dominant standards exist:
EIP-2771 — a minimal trusted forwarder pattern where the contract reads the original sender from the end of msg.data.
EIP-712 — typed structured data signing, which makes the signed payload human-readable in wallet UIs.
Meta-transactions are used in production by OpenSea (gasless NFT listings), Biconomy (relayer infrastructure), and Gelato Network (automated transaction relay). Understanding the implementation is essential for any dApp targeting mainstream users.
EIP-2771: Trusted Forwarder Pattern
EIP-2771 defines a minimal interface for trusted forwarders. The contract inherits from ERC2771Context (from OpenZeppelin) and overrides _msgSender() to extract the original signer from the calldata if the call came from a trusted forwarder.
The trusted forwarder:
1. Receives a ForwardRequest struct containing from, to, value, gas, nonce, and data.
2. Verifies the EIP-712 signature against the request.
3. Checks the nonce to prevent replay attacks.
4. Calls target with the original calldata appended with the signer's address.
The target contract reads _msgSender() — not msg.sender. If the caller is the trusted forwarder, _msgSender() returns the last 20 bytes of calldata (the original signer). Otherwise it returns msg.sender normally.
Critical security note: only one trusted forwarder address should be whitelisted. An attacker who controls a trusted forwarder can impersonate any user. Use the OpenZeppelin MinimalForwarder for a battle-tested implementation.
EIP-712 Typed Data Signing
EIP-712 is the standard for signing structured data. Instead of signing an opaque hash, the user sees a human-readable breakdown in their wallet: "Sign this to approve a transfer of 100 USDC to 0xabc...".
The domain separator encodes the contract name, version, chainId, and verifying contract address. This prevents signature replay across contracts and chains.
Defining a type in Solidity:
The typehash is the keccak256 of the type string: keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)")
Constructing the digest: keccak256(abi.encodePacked("", DOMAIN_SEPARATOR, structHash))
Verifying: use ECDSA.recover(digest, signature) and check the result equals the expected signer.
OpenZeppelin's EIP712 base contract handles domain separator construction and digest building. Always extend it rather than implementing EIP-712 from scratch.
Nonce Management and Replay Protection
Without nonce management, a signed meta-transaction can be submitted multiple times. Two nonce schemes exist:
Sequential nonces: a single counter per address. Transactions must be submitted in order. Simple but prevents out-of-order or concurrent submissions.
2D nonces (ERC-4337 style): a key-nonce pair. The key is arbitrary (often 0); the nonce is the counter within that key. Multiple independent queues can operate in parallel. This is the preferred approach for relayers handling many concurrent users.
Implementation in Solidity:
mapping(address => uint256) private _nonces; — sequential.
mapping(address => mapping(uint192 => uint256)) private _nonces; — 2D.
Expose a nonces(address owner) view function so frontends can fetch the current nonce before building a request.
Expiry: add a deadline field to the ForwardRequest and check block.timestamp <= deadline on-chain. This prevents old signed requests from being submitted later.
Setting Up a Relayer Infrastructure
Running your own relayer requires:
1. A funded EOA (the relayer wallet) with enough ETH to pay gas.
2. A backend service that receives meta-transaction requests, verifies signatures, checks business logic, and submits transactions.
3. Nonce and gas management to prevent stuck transactions.
Using Biconomy or Gelato eliminates the infrastructure overhead. Gelato's Relay SDK abstracts the entire flow — you call relay.sponsoredCall() from your frontend, Gelato handles submission, and you pay via an API credit balance.
Gas estimation: meta-transactions consume slightly more gas than direct calls due to the forwarder overhead (roughly 21,000 additional gas). Account for this in your gas limit calculations.
Monitor your relayer wallet balance. Implement alerts when it drops below a threshold. Failed relay submissions due to insufficient ETH result in a bad user experience with no clear error on the frontend.
For high-volume applications, use multiple relayer wallets with round-robin submission to avoid nonce conflicts from concurrent transactions.