Skip to content

ERC-2771

ERC-2771

Overview

ERC-2771 (Secure Protocol for Native Meta Transactions) is Ethereum's meta-transaction security protocol standard, defining a contract-level protocol that enables recipient contracts to securely accept meta transactions through a trusted Forwarder contract.

The core idea of meta transactions is to have third parties pay Gas fees on behalf of users, thereby improving the user experience. ERC-2771 standardizes this process, ensuring security and interoperability.

What Are Meta Transactions

Pain Points of Traditional Transactions In traditional Ethereum transactions: - Users must hold ETH to pay Gas fees - New users need to purchase ETH before they can use a DApp - Each operation requires the user to manually confirm and pay - This creates a high barrier for Web2 users

The Meta Transaction Solution Meta transactions allow: - Users to sign their transaction intent (off-chain) - A third party (Gas Relay) to submit and pay Gas on behalf of the user - Users to use DApps without holding ETH - A user experience similar to Web2

Roles in ERC-2771

Transaction Signer - The user who actually wants to perform the operation - Signs the transaction intent - Does not need to hold ETH

Gas Relay - Receives the user's signed message - Submits the transaction to the blockchain on behalf of the user - Pays the Gas fees - Can be compensated through other means

Forwarder Contract - Verifies the validity of the user's signature - Forwards the user's request to the target contract - Ensures it cannot be tampered with or forged - Trusted by the recipient contract

Recipient Contract - The contract that actually executes the business logic - Trusts a specific Forwarder contract - Obtains the real user address via _msgSender() - Obtains the real transaction data via _msgData()

How It Works

Meta Transaction Flow

  1. User Signs

    The user signs a message containing transaction parameters
    The signature includes: target contract, call data, nonce, etc.
    

  2. Submit to Relay

    The user sends the signed message to the Gas Relay (off-chain)
    The relay can be an application server or a dedicated relay service
    

  3. Relay Validates and Forwards

    The relay sends the signed request to the Forwarder contract
    The Forwarder verifies the validity of the signature
    

  4. Execute Business Logic

    The Forwarder calls the recipient contract
    The recipient contract extracts the real user address through a special method
    Executes the business logic
    

Core Interfaces

Forwarder Contract

interface IForwarder {
    struct ForwardRequest {
        address from;      // Transaction signer
        address to;        // Target contract
        uint256 value;     // ETH amount
        uint256 gas;       // Gas limit
        uint256 nonce;     // Anti-replay
        bytes data;        // Call data
    }

    function execute(
        ForwardRequest calldata req,
        bytes calldata signature
    ) external payable returns (bool, bytes memory);
}

Recipient Contract

abstract contract ERC2771Context {
    address private immutable _trustedForwarder;

    constructor(address trustedForwarder) {
        _trustedForwarder = trustedForwarder;
    }

    // Get the real message sender
    function _msgSender() internal view virtual override returns (address sender) {
        if (isTrustedForwarder(msg.sender)) {
            // Extract the real sender from the last 20 bytes of calldata
            assembly {
                sender := shr(96, calldataload(sub(calldatasize(), 20)))
            }
        } else {
            sender = msg.sender;
        }
    }

    // Get the real call data
    function _msgData() internal view virtual override returns (bytes calldata) {
        if (isTrustedForwarder(msg.sender)) {
            // Remove the last 20 bytes (sender address)
            return msg.data[:msg.data.length - 20];
        } else {
            return msg.data;
        }
    }

    function isTrustedForwarder(address forwarder) public view returns (bool) {
        return forwarder == _trustedForwarder;
    }
}

Security Considerations

Forwarder Trust Issue A malicious forwarder could: - Forge the address returned by _msgSender() - Make transactions appear to come from any address - Fully control the recipient contract

Defense Measures: - Recipient contracts only trust specific forwarders - Forwarder addresses are typically set as immutable at deployment time - Permissions to modify the trusted forwarder must be strictly limited - It is recommended that the forwarder list be immutable, or only modifiable by the contract owner

Replay Attacks The forwarder must implement a nonce mechanism: - Maintain an incrementing nonce for each user - Ensure each signature can only be used once - Prevent relays from resubmitting the same request

Signature Verification The forwarder must correctly verify signatures: - Use EIP-712 structured signing - Verify the signer address - Check the validity of the request

Practical Application Examples

Deploying a Forwarder

// Using OpenZeppelin's implementation
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";

contract MyForwarder is MinimalForwarder {}

Creating a Recipient Contract

import "@openzeppelin/contracts/metatx/ERC2771Context.sol";

contract MyContract is ERC2771Context {
    constructor(address trustedForwarder)
        ERC2771Context(trustedForwarder)
    {}

    function doSomething() public {
        // _msgSender() returns the real user address
        address user = _msgSender();

        // Execute business logic
        // ...
    }
}

Frontend Initiating a Meta Transaction

// 1. Construct forward request
const forwardRequest = {
    from: userAddress,
    to: contractAddress,
    value: 0,
    gas: 100000,
    nonce: await forwarder.getNonce(userAddress),
    data: contract.interface.encodeFunctionData('doSomething', [])
}

// 2. User signs (EIP-712)
const domain = {
    name: 'MinimalForwarder',
    version: '0.0.1',
    chainId: await provider.getNetwork().chainId,
    verifyingContract: forwarder.address
}

const types = {
    ForwardRequest: [
        { name: 'from', type: 'address' },
        { name: 'to', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'gas', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'data', type: 'bytes' }
    ]
}

const signature = await signer._signTypedData(domain, types, forwardRequest)

// 3. Send to relay server
await fetch('https://relay.example.com/execute', {
    method: 'POST',
    body: JSON.stringify({ forwardRequest, signature })
})

// 4. Relay server executes
// relayServer.execute(forwardRequest, signature)

Use Cases

Web3 Gaming - Players can play without holding ETH - Game developers pay Gas fees - Lowers the barrier for new users - Provides a smooth gaming experience

*NFT* Marketplaces** - Users can mint NFTs without ETH - Platforms subsidize Gas fees - Improves user conversion rates - Simplifies the purchase process

*DeFi* Protocols** - New users do not need to purchase ETH first - Protocols deduct Gas from transaction fees - Increases protocol adoption - Improves user experience

Enterprise Applications - Employees operate using company wallets - Company pays Gas fees centrally - Simplifies financial management - Improves operational efficiency

Notable Implementations

*OpenZeppelin* Provides a standard ERC-2771 implementation: - ERC2771Context: Recipient contract base class - MinimalForwarder: Simple forwarder implementation

Biconomy Professional meta-transaction infrastructure: - Provides a forwarder network - Supports the ERC-2771 standard - Developer-friendly SDK

Gelato Automation and relay service: - Supports meta transactions - Provides Gas sponsorship service - On-chain automation execution

Relationship with ERC-4337

Limitations of ERC-2771 - Contracts must explicitly support it (inherit ERC2771Context) - Forwarder selection and trust management is complex - Only supports EOA accounts

Advantages of ERC-4337 Account Abstraction (AA) provides a more comprehensive solution: - Native smart contract wallet support - More flexible Gas payment methods - Unified entry point contract - More powerful features (social recovery, batch operations, etc.)

Transition Trend Many ERC-2771 solutions are migrating to ERC-4337: - ERC-4337 is more universal and powerful - Better ecosystem support - A better choice in the long run - But ERC-2771 is still suitable for simple scenarios

Best Practices

Forwarder Management - Set the forwarder address as immutable - If it must be mutable, restrict modification to the contract owner only - Use multi-sig to manage forwarder updates - Record forwarder change history

Signature Security - Use EIP-712 structured signing - Include a domain separator to prevent cross-chain replay - Implement a nonce mechanism - Set reasonable expiration times

Gas Management - Estimate reasonable Gas limits - Prevent Gas exhaustion attacks - Implement a Gas fee compensation mechanism - Monitor relay costs

Contract Design - Use _msgSender() instead of msg.sender in all contract functions - Use _msgData() instead of msg.data - Consider backward compatibility - Test both forwarded and direct call scenarios

Future Outlook

Although ERC-4337 is becoming mainstream, ERC-2771 still has its value: - Simpler implementation - Lower Gas cost - Suitable for specific scenarios - Can coexist with ERC-4337