Skip to main content
Gelato is deprecating the trusted forwarder contracts. The old forwarder will no longer be available, and Gelato will not provide a replacement. To continue using ERC-2771 meta-transactions, you must deploy your own trusted forwarder and update your integration.

Migration Steps

1

Deploy Your Own Trusted Forwarder

Choose between two forwarder types based on your needs:
TypeReplay ProtectionUse Case
SequentialNonce (0, 1, 2…)Simple operations, ordered transactions
ConcurrentRandom salt (hash-based)Batch operations, parallel transactions
DISCLAIMER: All Solidity contracts in the referenced repository are provided as examples for educational purposes only. They have NOT been audited and may contain bugs or security vulnerabilities. USE AT YOUR OWN RISK. For production use, please ensure proper security audits are conducted by qualified professionals.
Sequential Forwarder (Nonce-based)Contract: TrusteForwarderERC2771.solConcurrent Forwarder (Hash-based)Contract: TrustedForwarderConcurrentERC2771.sol
Save your deployed forwarder address - you’ll need it for the next steps.
2

Whitelist the Trusted Forwarder in Your Contract

Your contract must trust the new forwarder address. How you do this depends on your contract’s architecture:If your contract has an updateable forwarder:
yourContract.setTrustedForwarder(newForwarderAddress);
If your contract has an immutable forwarder:You’ll need to redeploy your contract with the new forwarder address:
contract YourContract is ERC2771Context {
    constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}

    function yourFunction() external {
        address user = _msgSender();  // Still works the same
        // ...
    }
}
If your contract is not upgradeable and contains important state data, a migration strategy will be required to transfer the state to the new contract.
3

Update Frontend Encoding

Previously, Gelato handled the encoding to the trusted forwarder internally. Now you must encode the call to the forwarder yourself.
import { ethers } from "ethers";

// 1. Encode your function call
const functionData = yourContract.interface.encodeFunctionData("yourFunction", [args]);

// 2. Get user's nonce from YOUR forwarder
const userNonce = await trustedForwarder.userNonce(userAddress);

// 3. Create EIP-712 domain for YOUR forwarder
const domain = {
  name: "TrustedForwarder",
  version: "1",
  chainId: chainId,
  verifyingContract: trustedForwarderAddress  // YOUR forwarder address
};

// 4. Define the type structure
const types = {
  SponsoredCallERC2771: [
    { name: "chainId", type: "uint256" },
    { name: "target", type: "address" },
    { name: "data", type: "bytes" },
    { name: "user", type: "address" },
    { name: "userNonce", type: "uint256" },
    { name: "userDeadline", type: "uint256" }
  ]
};

// 5. Create the message
const message = {
  chainId: chainId,
  target: yourContractAddress,    // Your contract (the target)
  data: functionData,             // Your function call
  user: userAddress,
  userNonce: userNonce,           // From forwarder
  userDeadline: 0                 // 0 = no expiry
};

// 6. User signs the message
const signature = await signer.signTypedData(domain, types, message);

// 7. Encode the call to the forwarder
const forwarderData = trustedForwarder.interface.encodeFunctionData(
  "sponsoredCallERC2771",
  [
    message,           // The CallWithERC2771 struct
    sponsorAddress,    // Who pays (can be same as user)
    feeToken,          // Fee token address
    oneBalanceChainId, // Chain ID for 1Balance
    signature,         // User's signature
    0,                 // nativeToFeeTokenXRateNumerator
    0,                 // nativeToFeeTokenXRateDenominator
    ethers.ZeroHash    // correlationId
  ]
);

// 8. Send to Gelato with FORWARDER as target
await gelatoRelay.sponsoredCall({
  target: trustedForwarderAddress,  // YOUR forwarder, not your contract!
  data: forwarderData
});

Key Changes Summary

WhatBefore (Gelato handled it)After (You handle it)
ForwarderGelato’s forwarderYour deployed forwarder
EIP-712 Domain-Sign for YOUR forwarder
Domain name-"TrustedForwarder" or "TrustedForwarderConcurrentERC2771"
Domain verifyingContract-Your forwarder address
Get nonce from-Your forwarder (sequential only)
Gelato targetYour contractYour forwarder
EncodingJust your functionFull forwarder call

Sequential vs Concurrent

FeatureSequentialConcurrent
Replay protectionNonce (0, 1, 2…)Random salt
Transaction orderMust be in orderAny order
Parallel transactionsNoYes
Failed tx blocks othersYesNo
Get from forwarderuserNonce(address)Nothing (generate salt)
EIP-712 type nameSponsoredCallERC2771SponsoredCallConcurrentERC2771
Forwarder functionsponsoredCallERC2771()sponsoredCallConcurrentERC2771()

Migration Checklist

  • Deploy trusted forwarder (sequential or concurrent)
  • Whitelist forwarder in your contract (update address or redeploy)
  • Update frontend to encode calls to your forwarder
  • Test on testnet
  • Deploy to production

Troubleshooting

  • Ensure domain verifyingContract is your forwarder address (not your contract)
  • Ensure domain name matches exactly: "TrustedForwarder" or "TrustedForwarderConcurrentERC2771"
  • Ensure chainId matches the network
  • Get fresh nonce from forwarder before each signature: forwarder.userNonce(user)
  • Don’t reuse old signatures
  • Generate a new random userSalt for each transaction
  • Don’t reuse salts
  • Ensure your contract uses _msgSender() (from ERC2771Context)
  • Ensure the forwarder is whitelisted in your contract

Example Implementations

Example Contracts: