BLOG — Developer Tutorials

520 days ago

Gasless NFT Minting Using USDC: Powered by Gelato Relay

Non-Fungible Tokens (NFTs) have captivated the world and reshaped the way we perceive and trade digital assets. However, a persistent barrier of entry has hindered mass adoption – the need for a native token to facilitate transactions on the appropriate network.

By combining Gelato Relay with USDC, this process is streamlined, all whilst remaining trust minimized. In this post we will explore the simple process of integrating gasless minting into your next NFT project.

How it works

The user will sign an off-chain permit signature allowing the NFT contract to spend a specified amount of USDC.

The function arguments, along with the signature, are submitted to Gelato using the Gelato Relay SDK and are executed on-chain by a relayer.

The mint function uses the signature provided to permit itself to spend USDC; one USDC will be used to pay for the NFT itself and some is used to compensate the relayer. The permitted amount includes overhead to account for gas price fluctuations - the entire amount is rarely spent.

Requirements

There are only two requirements to support gasless minting, we must:

  1. Permit spending of USDC using an off-chain signature
  2. Compensate the relayer by paying a fee

By leveraging USDCs off-chain permit signatures, we can enable a truly gasless experience with Gelato Relay. Since the transaction is executed by a relayer on our behalf, the message sender is the relayer rather than the signer which imposes certain limitations.

Despite being authorized to facilitate interactions, smart contracts are not free to spend tokens on our behalf. Therefore, we must approve spending by target contracts first. Signing an off-chain permit signature proves our intent of allowing a contract to spend a certain number of tokens. The alternative is to “approve” the contract to spend tokens, but that requires a transaction on-chain and defeats the purpose.

The relayer puts transactions on-chain on our behalf, consequently incurring a gas cost that needs to be compensated. This is possible in one of two ways; sponsoring the transaction with 1Balance or by paying the relay fee synchronously within the transaction. The latter is ideal for our project as we can use USDC to pay for both the NFT, as well as the relay fee all within the same transaction.

Prerequisites

Ensure you have completed the following before you start:

  • Installed Node.js, NPM and Git
  • Set up a web3 wallet in your browser, such as MetaMask
  • RPC provider

Your Dev Environment

The full source code as well as instructions on how to deploy the contracts and the frontend for yourself are available here on GitHub.

Code Explanation

The NFT contract represents a typical ERC271 implementation. The deployer specifies the payment token (USDC) and amount in the constructor, and users call the “mint” function in order to mint new NFTs. This transfers payment tokens from the specified address and subsequently mints the NFT before incrementing the supply (the supply during the mint dictates the token ID). The contract keeps track of minted tokens and their ownership.

Traditional NFT Contract

Below is a minimal implementation of an ERC721 NFT Smart Contract requiring an ERC20 token for payment of the NFT, as well as a native gas token to transact on the network.

contract NFT is ERC721 {
    uint256 public price;
    uint256 public supply;
    ERC20 public token;
 
    constructor(ERC20 _token, uint256 _price)
        ERC721("NFT", "NFT")
    {
        price = _price;
        token = _token;
    }
 
    function mint(address to) external {
        token.transferFrom(to, address(this), price);
        _mint(to, supply++);
    }
}

Gasless NFT Contract

In order to support gasless minting, we must meet the requirements outlined. We must modify the “mint” function to take additional arguments, which get forwarded to the ERC20Permit token’s “permit” function.

This allows us to spend a user-specified number of tokens. With the spending approved, we transfer an appropriate relay fee to the fee collector. Since we transfer to the fee collector, we must restrict the mint function to prevent malicious actors from impersonating a relayer and draining the contract by quoting high relay fees.

To achieve this, we simply include the “onlyGelatoRelay” modifier in the function definition which is made available by inheriting from “GelatoRelayContext”. Note: Inheriting from “GelatoRelayContext” provides access to relay fee information (e.g., fee token, fee amount, fee collector) in addition to the “onlyGelatoRelay” modifier and “transferRelayFee” method.

The modified, and now gasless, NFT contract can be found below.

contract GaslessNFT is ERC721, GelatoRelayContext {
    uint256 public price;
    uint256 public supply;
    ERC20Permit public token;
 
    constructor(ERC20Permit _token, uint256 _price)
        ERC721("Gasless NFT", "GNFT")
    {
        price = _price;
        token = _token;
    }
 
    function mint(address to, uint256 amount, uint256 deadline, uint8 v,
        bytes32 r, bytes32 s) external onlyGelatoRelay
    {
        require(address(token) == _getFeeToken(),
            "GaslessNFT.mint: incorrect fee token");
 
        token.permit(to, address(this), amount, deadline, v, r, s);
        token.transferFrom(to, address(this), price);
 
        uint256 fee = _getFee();
        uint256 maxFee = amount - price;
 
        require(fee <= maxFee,
            "GaslessNFT.mint: insufficient fee");
 
        token.transferFrom(to, _getFeeCollector(), fee);
 
        _mint(to, supply++);
    }
}

Note: using ERC-2771 is generally recommended, but in this case, authentication is handled for us by USDC which is an implementation of the ERC-2612 permit extension.

Frontend implementation

The frontend is responsible for estimating the relay fee and signing a permit message allowing the NFT contract to spend a certain number of USDC (NFT price + relay fee).

Once signing is complete, it will construct the request and submit it to Gelato using “callWithSyncFee”. Estimating the relay fee and submitting the request are both handled by the Gelato Relay SDK.

const purchase = async () => {
   const wallet = new ethers.BrowserProvider(window.ethereum);
   const signer = await wallet.getSigner();
 
   const amount = price + fee;
   const deadline = ethers.MaxUint256;
 
   const { chainId } = await provider.getNetwork();
 
   const sig = await sign(signer, token, amount, nft, deadline, chainId);
   const { v, r, s } = sig;
 
   const { data } = await nft.mint.populateTransaction(
  	signer.address, amount, deadline, v, r, s);
 
   const request: CallWithSyncFeeRequest = {
      chainId: chainId.toString(),
      target: nft.target.toString(),
      feeToken: token.target.toString(),
      isRelayContext: true,
  	data: data
   };
 
   const { taskId } = await relay.callWithSyncFee(request);
};

Note: For increased clarity, certain checks have been omitted. The complete source code is available in this repository.

Conclusion

Gasless NFT minting paves the way for wider web3 adoption. By leveraging off-chain permit signatures and Gelato Relay, users can seamlessly transact on any network without requiring a native gas token. This streamlined process enhances user experience whilst remaining trust minimized - truly the best of both worlds!

About Gelato Relay

Gelato handles blockchain complexities for you every step of the way, enabling secure gasless transactions for a seamless user experience and onboarding.

Check out Relay as well as other Gelato services like Web3 Functions, Automate, and the Account Abstraction SDK.

While you're at it, join our community and our developer discussions on Discord!