The meta-transaction idea
Users sign an intent off-chain; a relayer broadcasts an on-chain transaction and pays gas. The contract recovers the signer from the signature and executes as if msg.sender were that user — or validates a signed allowance. This separates gas payer from action authorizer.
- Classic relayers are per-protocol — each dapp runs its own forwarder.
- Replay protection uses nonces tracked per signer in the forwarder contract.
- Relayers can censor — incentivize decentralization or allow self-relay.
EIP-2612 permit
ERC20Permit adds permit(owner, spender, value, deadline, v, r, s). Owner signs EIP-712 typed data; anyone submits it on-chain to set allowance. Enables approve + action in one transaction for DEX deposits, staking, etc.
- DOMAIN_SEPARATOR includes name, version, chainId, verifyingContract.
- Permit nonces increment per owner — order matters for pending signatures.
- Deadline prevents stale signatures from being submitted later.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract MyToken is ERC20, ERC20Permit {
constructor()
ERC20("My Token", "MTK")
ERC20Permit("My Token")
{}
}EIP-712 typed signatures
Wallets display structured fields users can read — safer than blind eth_sign. Contracts rebuild the digest with DOMAIN_SEPARATOR and struct type hashes. Wrong chain id or contract address invalidates the signature — protects cross-chain replay.
- name and version in permit must match token metadata.
- Hardware wallets show typed fields — verify spender address carefully.
- EIP-5267 eip712Domain() helps integrators discover domain parameters.
ERC-2771 forwarders
OpenZeppelin ERC2771Context appends signer address to calldata for trusted forwarder contracts. _msgSender() returns the signer instead of the relayer. Only whitelisted forwarders should be trusted — a malicious forwarder can forge sender context.
- isTrustedForwarder(forwarder) gates which relayers are accepted.
- MinimalForwarder is simple; ERC2771Forwarder adds EIP-712 request structs.
- Recipient contracts must inherit ERC2771Context — otherwise relayer is msg.sender.
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
abstract contract Recipient is ERC2771Context {
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
}ERC-4337 vs classic meta-tx
Account abstraction (ERC-4337) uses smart contract wallets and UserOperations submitted to bundlers via an EntryPoint contract. Validation logic lives in the wallet — social recovery, paymasters, batching. It generalizes relayers instead of one forwarder per dapp.
- Paymasters sponsor gas or accept ERC20 fees — see AA article.
- Bundler network is still maturing — check chain support.
- EOAs remain dominant — permit is still the main gasless approval path today.
Integration checklist
Before shipping permit UX: confirm token implements EIP-2612, fetch nonces on-chain, set reasonable deadlines, display spender and value in human units, handle tokens without permit gracefully (fallback to approve). Never store private keys server-side to 'relay' for users.
- Simulate permit + action with eth_call before spending gas.
- Some tokens use legacy name in DOMAIN — match deployment, not branding.
- Permit2 (Uniswap) centralizes allowances across tokens — different trust model.
