Migrate from Gelato’s deprecated trusted forwarder to your own forwarder
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.
Choose between two forwarder types based on your needs:
Type
Replay Protection
Use Case
Sequential
Nonce (0, 1, 2…)
Simple operations, ordered transactions
Concurrent
Random 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.
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:
If your contract has an immutable forwarder:You’ll need to redeploy your contract with the new forwarder address:
Copy
Ask AI
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.
Sequential Forwarder
Concurrent Forwarder
Copy
Ask AI
import { ethers } from "ethers";// 1. Encode your function callconst functionData = yourContract.interface.encodeFunctionData("yourFunction", [args]);// 2. Get user's nonce from YOUR forwarderconst userNonce = await trustedForwarder.userNonce(userAddress);// 3. Create EIP-712 domain for YOUR forwarderconst domain = { name: "TrustedForwarder", version: "1", chainId: chainId, verifyingContract: trustedForwarderAddress // YOUR forwarder address};// 4. Define the type structureconst 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 messageconst 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 messageconst signature = await signer.signTypedData(domain, types, message);// 7. Encode the call to the forwarderconst 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 targetawait gelatoRelay.sponsoredCall({ target: trustedForwarderAddress, // YOUR forwarder, not your contract! data: forwarderData});
Copy
Ask AI
import { ethers } from "ethers";// 1. Encode your function callconst functionData = yourContract.interface.encodeFunctionData("yourFunction", [args]);// 2. Generate a unique salt (random bytes32)const userSalt = ethers.hexlify(ethers.randomBytes(32));// 3. Create EIP-712 domain for YOUR forwarderconst domain = { name: "TrustedForwarderConcurrentERC2771", version: "1", chainId: chainId, verifyingContract: trustedForwarderAddress // YOUR forwarder address};// 4. Define the type structureconst types = { SponsoredCallConcurrentERC2771: [ { name: "chainId", type: "uint256" }, { name: "target", type: "address" }, { name: "data", type: "bytes" }, { name: "user", type: "address" }, { name: "userSalt", type: "bytes32" }, { name: "userDeadline", type: "uint256" } ]};// 5. Create the messageconst message = { chainId: chainId, target: yourContractAddress, // Your contract (the target) data: functionData, // Your function call user: userAddress, userSalt: userSalt, // Random salt for replay protection userDeadline: 0 // 0 = no expiry};// 6. User signs the messageconst signature = await signer.signTypedData(domain, types, message);// 7. Encode the call to the forwarderconst forwarderData = trustedForwarder.interface.encodeFunctionData( "sponsoredCallConcurrentERC2771", [ message, // The CallWithConcurrentERC2771 struct sponsorAddress, // Who pays 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 targetawait gelatoRelay.sponsoredCall({ target: trustedForwarderAddress, // YOUR forwarder, not your contract! data: forwarderData});