1️⃣sponsoredCallERC2771

Sponsored transactions with ERC2771 authentication support

If you plan to use ERC-2771 with a multicall method or any other method using delegateCall()Please read carefully the section Avoid ERC-2771-risks

If you are using @gelatonetwork/relay-sdk v3 or contracts from the package @gelatonetwork/relay-context v2 please follow this migration guide to migrate to the new versions.

After reading this page:

  • You'll know how to use the sponsoredCallERC2771 SDK method. This will give your user's a gasless UX requiring a user signature. This uses the 1Balance payment method, allowing you to sponsor some/all of your user's gas costs.

  • You'll learn about how to incorporate ERC2771Context into your contract for _msgSender() support.

  • You'll see some code which will help you send a relay request within minutes.

Overview

The sponsoredCallERC2771 method uses both a sponsor API key and a user's signature, like that from MetaMask, to securely sponsor gasless transactions. Payments are made via the Gelato 1Balance method.

Gelato Relay SDK has various methods for handling sponsored ERC2771 transactions. The most straightforward is sponsoredCallERC2771, which handles both signing and sending in one step. If you need to separate these processes, other SDK methods are available.

SDK Methods

sponsoredCallERC2771

This method initiates the signing of ERC2771 requests with the provided BrowserProvider or Wallet. Once the signature is obtained, the request is forwarded to Gelato.

const sponsoredCallERC2771 = async (
  request: CallWithERC2771Request | CallWithConcurrentERC2771Request,
  signerOrProvider: ethers.BrowserProvider | ethers.Signer,
  sponsorApiKey: string,
  options?: RelayRequestOptions
): Promise<RelayResponse>

Arguments

  • request: The body of the request intended for sending.

  • signerOrProvider: a valid provider connected to RPC or a signer.

  • sponsorApiKey: an API key used to authenticate your sponsorship.

  • options: an object for specifying optional parameters.

Response

type RelayResponse = {
  taskId: string;
};

getSignatureDataERC2771

This method starts the signing process for ERC2771 requests using the given BrowserProvider or Signer. After capturing the signature, it returns both the signature and the message. This collected data can then be used with the sponsoredCallERC2771WithSignature method to send the request to Gelato.

getSignatureDataERC2771 = (
  request: CallWithERC2771Request | CallWithConcurrentERC2771Request,
  signerOrProvider: ethers.BrowserProvider | ethers.Signer,
  type: ERC2771Type
): Promise<SignatureData>

Arguments

  • request: this is the request body used to send a request.

  • signerOrProvider: a valid provider connected to RPC or a signer.

  • type: SponsoredCall for a sequential flow or ConcurrentSponsoredCall for a concurrent flow.

Response

type SignatureData = ConcurrentSignatureData 
| SequentialSignatureData;

type ConcurrentSignatureData = {
  struct: CallWithConcurrentERC2771Struct;
  signature: string;
};

type SequentialSignatureData = {
  struct: CallWithERC2771Struct;
  signature: string;
};
  • struct: EIP-712 message data.

  • signature: EIP-712 signature.

getDataToSignERC2771

This method provides the message data intended for external signing along with the EIP-712 typed data. After obtaining the signature, the request can be dispatched using the sponsoredCallERC2771WithSignature method.

getDataToSignERC2771 = (
  request: CallWithERC2771Request | CallWithConcurrentERC2771Request,
  type: ERC2771Type,
  signerOrProvider?: ethers.BrowserProvider | ethers.Signer,
): Promise<PayloadToSign>

Arguments

  • request: The body of the request intended for sending.

  • type: SponsoredCall for a sequential flow or ConcurrentSponsoredCall for a concurrent flow.

  • signerOrProvider (optional): A provider needed in a sequential flow to obtain the nonce from the smart contract. If you're providing the nonce within your request or if you're using the concurrent flow, this parameter isn't necessary.

Response

type PayloadToSign = ConcurrentPayloadToSign | SequentialPayloadToSign;

type ConcurrentPayloadToSign = {
  struct: CallWithConcurrentERC2771Struct;
  typedData: SponsoredCallConcurrentERC2771PayloadToSign 
};

type SequentialPayloadToSign = {
  struct: CallWithERC2771Struct;
  typedData: SponsoredCallERC2771PayloadToSign
};
  • struct: EIP-712 message data.

  • typedData: EIP-712 typed data.

sponsoredCallERC2771WithSignature

This method sends pre-signed requests to Gelato.

sponsoredCallERC2771WithSignature = async (
  struct: SignatureData["struct"],
  signature: SignatureData["signature"],
  sponsorApiKey: string,
  options?: RelayRequestOptions
): Promise<RelayResponse>

Arguments

  • struct: EIP-712 message data returned from the signing methods.

  • signature: EIP-712 signature returned after signing the request.

  • sponsorApiKey: an API key used to authenticate your sponsorship.

  • options: an object for specifying optional parameters.

Response

type RelayResponse = {
  taskId: string;
};

Optional Parameters

See Optional Parameters.

Sending a Request

As of today, we support two distinct ways of sending sponsoredCallERC2771 requests:

  1. Sequentially: This approach ensures that each request is ordered and validated against the nonce stored on-chain. You have two options in this method:

    • Fetch the current nonce value from the smart contract yourself and include it with your request.

    • Allow the relay-sdk to fetch the nonce value for you when handling your relay request.

  2. Concurrently: This method enables you to send multiple transactions simultaneously. Replay protection is achieved using a hash-based salt mechanism. Again, you have two options:

    • Provide your own salt value.

    • Allow the relay-sdk to generate a unique salt value for you when processing your relay request.

By default sponsoredCallERC2771 requests are using the sequential method.

Concurrent ERC2771 support has been introduced in the relay-sdk version 5.1.0. Please make sure that your package is up-to-date to start using it.

Request Body

type SequentialERC2771Request = {
  chainId: BigNumberish;
  target: string;
  data: BytesLike;
  user: string;
  userDeadline?: BigNumberish;
  isConcurrent?: false;
  userNonce?: BigNumberish;
};
type ConcurrentERC2771Request = {
  chainId: BigNumberish;
  target: string;
  data: BytesLike;
  user: string;
  userDeadline?: BigNumberish;
  isConcurrent: true;
  userSalt?: string
};

Common Parameters:

  • chainId: the chain ID of the chain where the target smart contract is deployed.

  • target: the address of the target smart contract.

  • data: encoded payload data (usually a function selector plus the required arguments) used to call the required target address.

  • user: the address of the user's EOA.

  • userDeadline: optional, the amount of time in seconds that a user is willing for the relay call to be active in the relay backend before it is dismissed.

    • This way the user knows that if the transaction is not sent within a certain timeframe, it will expire. Without this, an adversary could pick up the transaction in the mempool and send it later. This could transfer money, or change state at a point in time which would be highly undesirable to the user.

Parameters For Sequential Requests:

  • isConcurrent: false (default), optional, represents that the users' requests are validated based on a nonce, which enforces them to be processed sequentially.

  • userNonce: optional, this nonce, akin to Ethereum nonces, is stored in a local mapping on the relay contracts. It serves to enforce the nonce ordering of relay calls if the user requires sequential processing. If this parameter is omitted, the relay-sdk will automatically query the current value on-chain.

Parameters For Concurrent Requests:

  • isConcurrent: true, indicates that the users' requests are validated based on a unique salt, allowing them to be processed concurrently. Replay protection is still ensured by permitting each salt value to be used only once.

  • userSalt: optional, this is a bytes32 hash that is used for replay protection. If the salt is not provided then relay-sdk would generate a unique value based on a random seed and a timestamp.

Example Code

For your testing, Gelato has deployed a simple contract which implements logic to increment a counter with ERC2771 support.

  • CounterERC2771.sol: deployed at the address 0x00172f67db60E5fA346e599cdE675f0ca213b47b on these networks.

CounterERC2771.sol's counter is special because it implements ERC-2771 _msgSender authentication to allow for secure whitelisting based on the identity of the original off-chain relay request originator, which has been verified using a user signature.

Furthermore, to set your trusted forwarder, you need the address for GelatoRelay1BalanceERC2771.sol that you can find here.

1. Deploy an ERC2771Context compatible contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {
    ERC2771Context
} from "@gelatonetwork/relay-context/contracts/vendor/ERC2771Context.sol";

// Importing ERC2771Context gives access to:
// 1. An immutable trusted forwarder address
// 2. function isTrustedForwarder 
//    to verify an input address matches the trustedForwarder address
// 3. function _msgSender()
//    which decodes the user's address from the calldata
//    _msgSender() can now be used to refer to user safely
//    instead of msg.sender (which is Gelato Relay in this case).
// 4. function _msgData()
//    which decodes the function signature from the calldata
contract CounterERC2771 is ERC2771Context {
    // Here we have a mapping that maps a counter to an address
    mapping(address => uint256) public contextCounter;

    event IncrementCounter(address _msgSender);

    // ERC2771Context: setting the immutable trustedForwarder variable
    constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
    
    // `incrementContext` is the target function to call
    // This function increments a counter variable which is 
    // mapped to every _msgSender(), the address of the user.
    // This way each user off-chain has their own counter 
    // variable on-chain.
    function increment() external {
        // Remember that with the context shift of relaying,
        // where we would use `msg.sender` before, 
        // this now refers to Gelato Relay's address, 
        // and to find the address of the user, 
        // which has been verified using a signature,
        // please use _msgSender()!

        // If this contract was not not called by the 
        // trusted forwarder, _msgSender() will simply return 
        // the value of msg.sender instead.
        
        // Incrementing the counter mapped to the _msgSender!
        contextCounter[_msgSender()]++;
        
        // Emitting an event for testing purposes
        emit IncrementCounter(_msgSender());
    }
}

2. Import GelatoRelaySDK into your front-end .js project

import { GelatoRelay, SponsoredCallERC2771Request } from "@gelatonetwork/relay-sdk";
const relay = new GelatoRelay();

3. Send the payload to Gelato

This is an example using Gelato's CounterERC2771.sol which is deployed on these networks.

// Set up on-chain variables, such as target address
const counter = "0x00172f67db60E5fA346e599cdE675f0ca213b47b"; 
const abi = ["function increment()"];
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const user = await signer.getAddress();

// Generate the target payload
const contract = new ethers.Contract(counter, abi, signer);
const { data } = await contract.incrementContext.populateTransaction();

// Populate a relay request
const request: CallWithERC2771Request = {
  chainId: (await provider.getNetwork()).chainId,
  target: counter;
  data: data;
  user: user;
};

// Without a specific API key, the relay request will fail! 
// Go to https://relay.gelato.network to get a testnet API key with 1Balance.
// Send a relay request using Gelato Relay!
const relayResponse = await relay.sponsoredCallERC2771(request, provider, apiKey);

Last updated