Integration Steps

Learn about the steps required to start using Cross-Chain Token Gating

We're in Beta!

0xEssential is excited to help developers explore Cross-Chain Token Gating but we are still polishing the solution. We have not received an audit and plan on making performance improvements as things settle.

Implementing cross-chain token gating is a full-stack project. You cannot use this solution for contracts that are already deployed.

You will need smart contract development knowledge, backend and devops capabilities for deploying your relaying service, plus dApp development skills for implementing meta-transactions with our client SDK. You will also need some funds - while the 0xEssential software is free, our solution requires that you pay transaction fees for your users.

We try to design our software to be as easy to use as possible, without sacrificing security, openness or decentralization. A full-stack web3 developer who is familiar with meta-transactions can use our OSS and example code to set up a proof of concept in a couple of hours.

We suggest approaching the project from the back of the stack frontwards - start by understanding how to write your L2 contract, then set up your relayer service, and finally use our client SDK in your dApp (or contract test suite!).

1. Build Your Implementation Contract

Your Implementation Contract is the contract deployed on an L2 that includes your business logic - your game, voting program or raffle entry - whatever program you're writing that needs cheap tx fees while also depending on data about NFT ownership on L1 or other chains.

0xEssential currently supports Polygon's Matic mainnet and Mumbai test networks, so you'll need to deploy your implementation contracts there. Let us know what other chains you'd like us to support!

Your Implementation Contract must inherit 0xEssential's context primitive EssentialERC2771Context - if you've used meta-transactions before you'll be familiar with using _msgSender() to get the caller rather than msg.sender. Our context primitive adds a function _msgNFT() that returns a struct with the NFT chain ID, contract address and token ID data that has been verified by our Ownership API.

You will need to specify the address of the 0xEssential Forwarder in the constructor for your Implementation Contract. Only specify addresses that you know are performing the trust-minimized OffchainLookup against our API. We recommend using our deployed Forwarder, but developers may prefer to deploy their own.

Here's a minimal example of an Implementation Contract that allows an NFT holder to "count" each NFT they own once. We import and inherit EssentialERC2771Context from the 0xEssential contracts NPM package, and import the IForwardRequest interface so we can access the NFT struct returned by _msgNFT(). We can also use the onlyForwarder modifier when writing functions that depend on verified NFT data and should only be called from the EssentialForwarder.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.9;

import "@0xessential/contracts/fwd/EssentialERC2771Context.sol";
import "@0xessential/contracts/fwd/IForwardRequest.sol";

contract Counter is EssentialERC2771Context {
    uint256 public totalCount;
    mapping(uint256 => mapping(address => mapping(uint256 => address))) internal registeredNFTs;

    event Counted(address indexed contractAddress, uint256 indexed tokenId, address indexed counter);

    constructor(address trustedForwarder) EssentialERC2771Context(trustedForwarder) {}

    function increment() external onlyForwarder {
        // this function will only be called if 
        // _msgSender() currently owns _msgNFT(),
        address owner = _msgSender();
        IForwardRequest.NFT memory nft = _msgNFT();

        require(
            registeredNFTs[nft.chainId][nft.contractAddress][nft.tokenId] == address(0),
            "NFT already counted"
        );
        
        registeredNFTs[nft.chainId][nft.contractAddress][nft.tokenId] = owner;

        unchecked {
            ++totalCount;
        }

        emit Counted(nft.contractAddress, nft.tokenId, owner);
    }
}

Check out the essential-contracts repo on Github, and read our guide on using EssentialERC2771Context with your Implementation Contract.

2. Setup Your Relayer Service

When implementing meta-transactions, your users sign a message via your dApp. You must then take this signature and pass it to a backend relaying service. The relaying service is responsible for submitting the transaction to the network on behalf of the signer, paying the transaction fee.

0xEssential provides OSS code that you can deploy to OpenZeppelin Defender as an Autotask. You can also use this code to build your own relaying service, but we recommend OpenZeppelin for security and transaction retry functionality.

You'll need to ensure your relaying API is secure - your relaying service will be submitting transactions, so you will need private keys and the EOA you use for relaying needs to hold funds for the network gas token, i.e. $MATIC. You also want to ensure only requests you're willing to pay for are accepted and submitted by your relayer.

0xEssential uses OpenZeppelin Defender, and we proxy requests from our frontend through an API route that restricts CORS requests. This way we don't expose the URI or credentials for our Defender Autotask, and only requests made from our frontend are valid.

Check out the essential-autotasks repo on Github, and read our guide on Using essential-autotasks with OpenZeppelin Defender.

3. Connect Your Frontend

Once your relayer service is in place and you've deployed a contract to a testnet, you'll use 0xEssential's SDK to implement meta-transaction signing in your dApp.

Our SDK extends ethers with an EssentialSigner class. You'll use this class to wrap a Web3Provider and connect to your Implementation Contract - EssentialSigner will handle signatures and submitting them to your Relayer.

When you call a function on your implementation contract, you use the customData override key to provide the chain ID, contract address and token ID of the NFT being used.

Here's how we would call the increment function from the Counter contract above:

import { Contract } from '@ethersproject/contracts';
import { EssentialSigner } from '@0xessential/signers';

import Counter from '../../abis/Counter.json';
import { Counter as TCounter } from '../../typechain';

const increment = async (nftContract: string, nftTokenId: string) => {
  const essentialCounter = new Contract(
    Counter.address,
    Counter.abi,
    // developers are reponsible for having the user 
    // connect their wallet in order to provide the
    // address and Web3Provider
    new EssentialSigner(address, provider),
  ) as TCounter;

  const { hash } = await essentialCounter.increment({
    customData: {
      nftChainId: '1',
      nftContract,
      nftTokenId,
    },
  });
};

Note that our message signing is network agnostic - the address and provider you pass to the EssentialSigner constructor should be the address and Web3Provider for a connected EOA. The EOA's wallet can be connected to any EVM chain - our solution performs validation of the NFT and implementation contract chain ID throughout the flow.

This call will result in the user's wallet asking them to sign an EIP-712 compliant meta-transaction payload. The signature and payload are then submitted to your process.env.RELAYER_URI - in development you can use an OpenZeppelin Autotask Webhook URI directly, but review the security considerations for your relayer service.

Check out the essential-signer repo on Github, and read our guide on Using essential-signer in Your dApp.

4. Burner Wallet Integration

Coming Soon!

For the best user experience, particularly in gaming, 0xEssential's Cross-Chain Token Gating supports using a persistent burner wallet for message signing, combined with primary EOA authorization for the burner to use the primary's NFTs. If you're building L2 games with L1 NFTs, get in touch with us to learn more!

Last updated