CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-openzeppelin-solidity

Secure Smart Contract library providing battle-tested implementations of industry-standard Solidity contracts including ERC20, ERC721, ERC1155 tokens, access control mechanisms, proxy patterns, and governance systems for Ethereum and EVM-compatible blockchains.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

metatx.mddocs/

Meta-Transactions

OpenZeppelin Contracts provides ERC-2771 meta-transaction support enabling gasless transactions and improved user experience in decentralized applications by allowing third parties to pay gas fees on behalf of users.

Core Imports

Import meta-transaction contracts using Solidity import statements:

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

Capabilities

ERC2771 Context

Context variant that supports meta-transactions by extracting the actual sender from the call data when transactions are forwarded through trusted forwarder contracts.

abstract contract ERC2771Context is Context {
    constructor(address trustedForwarder);
    
    function isTrustedForwarder(address forwarder) public view virtual returns (bool);
    function _msgSender() internal view virtual override returns (address sender);
    function _msgData() internal view virtual override returns (bytes calldata);
}

Usage Example

pragma solidity ^0.8.0;

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

contract MetaToken is ERC20, ERC2771Context {
    constructor(
        string memory name,
        string memory symbol,
        address trustedForwarder
    ) ERC20(name, symbol) ERC2771Context(trustedForwarder) {
        _mint(msg.sender, 1000000 * 10**18);
    }
    
    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender(); // Gets actual sender, even in meta-tx
        _transfer(owner, to, amount);
        return true;
    }
    
    function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address) {
        return ERC2771Context._msgSender();
    }
    
    function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) {
        return ERC2771Context._msgData();
    }
}

Minimal Forwarder

Simple implementation of a meta-transaction forwarder that verifies signatures and executes calls on behalf of users.

contract MinimalForwarder is EIP712 {
    struct ForwardRequest {
        address from;
        address to;
        uint256 value;
        uint256 gas;
        uint256 nonce;
        bytes data;
    }
    
    function getNonce(address from) public view returns (uint256);
    function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool);
    function execute(ForwardRequest calldata req, bytes calldata signature) public payable returns (bool, bytes memory);
}

Events

event ExecutedForwardRequest(address indexed from, uint256 nonce, bool success);

Usage Example

pragma solidity ^0.8.0;

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

// Deploy the forwarder
contract MetaTxSetup {
    MinimalForwarder public forwarder;
    MetaToken public token;
    
    constructor() {
        forwarder = new MinimalForwarder();
        token = new MetaToken("MetaToken", "META", address(forwarder));
    }
    
    function executeMetaTransaction(
        MinimalForwarder.ForwardRequest calldata req,
        bytes calldata signature
    ) external {
        require(forwarder.verify(req, signature), "Invalid signature");
        forwarder.execute(req, signature);
    }
}

Client-Side Meta-Transaction Implementation

JavaScript/TypeScript Example

// Client-side code for creating meta-transactions
const ethers = require('ethers');

class MetaTransactionClient {
    constructor(forwarderAddress, forwarderAbi, signer) {
        this.forwarder = new ethers.Contract(forwarderAddress, forwarderAbi, signer);
        this.signer = signer;
    }
    
    async createMetaTransaction(to, data, value = 0, gas = 100000) {
        const from = await this.signer.getAddress();
        const nonce = await this.forwarder.getNonce(from);
        
        const request = {
            from,
            to,
            value,
            gas,
            nonce,
            data
        };
        
        // Create EIP-712 typed data
        const domain = {
            name: 'MinimalForwarder',
            version: '0.0.1',
            chainId: await this.signer.getChainId(),
            verifyingContract: this.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' }
            ]
        };
        
        // Sign the meta-transaction
        const signature = await this.signer._signTypedData(domain, types, request);
        
        return { request, signature };
    }
    
    async executeMetaTransaction(request, signature) {
        return await this.forwarder.execute(request, signature);
    }
}

// Usage
async function sendMetaTransaction() {
    const client = new MetaTransactionClient(forwarderAddress, forwarderAbi, userSigner);
    
    // Create a token transfer call
    const tokenInterface = new ethers.utils.Interface(['function transfer(address,uint256)']);
    const data = tokenInterface.encodeFunctionData('transfer', [recipient, amount]);
    
    const { request, signature } = await client.createMetaTransaction(
        tokenAddress,
        data
    );
    
    // Send to relayer or execute directly
    await client.executeMetaTransaction(request, signature);
}

Advanced Meta-Transaction Patterns

Batch Meta-Transactions

pragma solidity ^0.8.0;

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

contract BatchForwarder is MinimalForwarder {
    struct BatchRequest {
        ForwardRequest[] requests;
        uint256 deadline;
    }
    
    function executeBatch(
        BatchRequest calldata batchReq,
        bytes[] calldata signatures
    ) external returns (bool[] memory successes, bytes[] memory results) {
        require(block.timestamp <= batchReq.deadline, "Batch expired");
        require(batchReq.requests.length == signatures.length, "Length mismatch");
        
        successes = new bool[](batchReq.requests.length);
        results = new bytes[](batchReq.requests.length);
        
        for (uint256 i = 0; i < batchReq.requests.length; i++) {
            require(verify(batchReq.requests[i], signatures[i]), "Invalid signature");
            (successes[i], results[i]) = execute(batchReq.requests[i], signatures[i]);
        }
    }
}

Conditional Meta-Transactions

pragma solidity ^0.8.0;

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

contract ConditionalMetaTx is ERC2771Context {
    mapping(address => uint256) public balances;
    mapping(bytes32 => bool) public executedConditions;
    
    struct ConditionalTransfer {
        address from;
        address to;
        uint256 amount;
        uint256 minBalance;
        uint256 deadline;
        bytes32 conditionHash;
    }
    
    constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
    
    function executeConditionalTransfer(
        ConditionalTransfer calldata transfer
    ) external {
        require(block.timestamp <= transfer.deadline, "Transfer expired");
        require(!executedConditions[transfer.conditionHash], "Already executed");
        require(balances[transfer.from] >= transfer.minBalance, "Condition not met");
        require(_msgSender() == transfer.from, "Unauthorized");
        
        executedConditions[transfer.conditionHash] = true;
        balances[transfer.from] -= transfer.amount;
        balances[transfer.to] += transfer.amount;
    }
}

Gasless NFT Minting

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract GaslessNFT is ERC721, ERC2771Context {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    
    mapping(address => bool) public hasMinted;
    
    constructor(address trustedForwarder) 
        ERC721("GaslessNFT", "GNFT") 
        ERC2771Context(trustedForwarder) 
    {}
    
    function mint() external {
        address user = _msgSender();
        require(!hasMinted[user], "Already minted");
        
        _tokenIds.increment();
        uint256 tokenId = _tokenIds.current();
        
        hasMinted[user] = true;
        _safeMint(user, tokenId);
    }
    
    function _msgSender() internal view virtual override(Context, ERC2771Context) returns (address) {
        return ERC2771Context._msgSender();
    }
    
    function _msgData() internal view virtual override(Context, ERC2771Context) returns (bytes calldata) {
        return ERC2771Context._msgData();
    }
}

Meta-Transaction Best Practices

  1. Trusted Forwarders: Only use well-audited forwarder contracts
  2. Nonce Management: Implement proper nonce tracking to prevent replay attacks
  3. Signature Verification: Always verify signatures before execution
  4. Gas Limits: Set appropriate gas limits for forwarded transactions
  5. Deadline Protection: Include deadlines to prevent stale transaction execution
  6. Cost Considerations: Factor in the additional gas costs of meta-transactions

Integration with Existing Contracts

Upgrading to Meta-Transaction Support

// Before: Regular contract
contract RegularContract {
    function doSomething() external {
        // msg.sender is the actual caller
        require(msg.sender == owner, "Not owner");
    }
}

// After: Meta-transaction enabled contract
contract MetaEnabledContract is ERC2771Context {
    constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
    
    function doSomething() external {
        // _msgSender() works for both regular and meta-transactions
        require(_msgSender() == owner, "Not owner");
    }
}

Relayer Infrastructure

Meta-transactions typically require relayer services that:

  1. Accept signed meta-transactions from users
  2. Pay gas fees to execute transactions on-chain
  3. Potentially charge fees or use other monetization strategies
  4. Provide APIs for dApp integration

Security Considerations

  1. Signature Replay: Implement proper nonce mechanisms
  2. Forwarder Trust: Only trust audited forwarder contracts
  3. Gas Griefing: Implement gas limit controls
  4. Fee Extraction: Be aware of MEV and fee extraction risks
  5. Contract Upgrades: Consider meta-transaction compatibility in upgrades

Error Handling

Meta-transaction contracts may revert with various errors:

  • MinimalForwarder: Signature verification failures, nonce mismatches, insufficient gas
  • ERC2771Context: No specific errors, but underlying contract logic may fail
  • General: All standard contract errors apply, plus meta-transaction specific validation failures

Install with Tessl CLI

npx tessl i tessl/npm-openzeppelin-solidity

docs

access-control.md

crosschain.md

finance.md

governance.md

index.md

metatx.md

proxy.md

security.md

tokens.md

utilities.md

tile.json