EssentialContext

Inherit EssentialContext in your L2 contract

You must inherit EssentialContext in your Layer 2 contract to enable NFT Global Entry. If you've used meta-transactions in the past you will be familiar with this process. Even if you are not offering Gasless Transactions, you must follow this guide for NFT Global Entry. Gasless Transactions can be added later without any contract updates. There are a few important things to know when inheriting EssentialContext for a Global Entry integration:

  1. Transactions that depend on Global Entry must be restricted to calls from an EssentialForwarder contract. Without this restriction, any caller could spoof NFT authorization and make unauthorized calls to your contract. EssentialContext includes a modifier onlyForwarder for this purpose. Global Entry uses a Forwarding contract for both gasless transactions and standard transactions.

  2. When properly restricted, your functions that depend on Global Entry will only be called by an EssentialForwarder when the submitter has proper NFT authorization. In other words, your contract can assume that _msgSender() owns or is authorized to use _msgNFT().

  3. When writing functions that depend on a specific NFT, do not use function parameters for NFT data. Your function will call _msgNFT() to get a struct that represents the NFT being "used" in the transaction, including the chain ID, contract address and token ID of the NFT.

Installation

EssentialContext and a version compatible with upgradeable proxy contracts EssentialContextUpgradeable are available in our contracts SDK. Source code is available on Github.

You can install from NPM with

yarn add @xessential/contracts

or

npm install @xessential/contracts

You can also install the package from Github in a foundry project:

forge install 0xessential/xessential

Then, update your remappings to handle our monorepo structure:

# remappings.txt
@xessential/contracts=lib/xessential/packages/contracts/contracts/fwd

Constructor

EssentialContext must be initialized in your constructor with the address of the EssentialForwarder you're using.

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

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

contract MyContract is EssentialERC2771Context {
    constructor(address trustedForwarder) EssentialERC2771Context(trustedForwarder) {
        // Your other constructor logic
    }
}

If you're using 0xEssential's canonical EssentialForwarder you can use this address across every network we support:

0x000000000066b3aED7Ae8263588dA67fF381FfCa

If you're using Global Entry in an upgradeable proxy we assume you already understand how to use the upgradeable flavor with an initialization call.

Modifier

EssentialContext provides a modifier and other convenience functions for restricting calls to your contract. You must restrict calls that depend on NFT authorization to only be called by an EssentialForwarder otherwise anyone can spoof NFT ownership data and manipulate your contract.

You can add the onlyForwarder modifier to any function that depends on NFT authorization:

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

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

contract MyContract is EssentialERC2771Context {
    constructor(address trustedForwarder) EssentialERC2771Context(trustedForwarder) {
        // Your other constructor logic
    }
    
    function tokenGatedFunction() external  {
    }
}

Forwarder Admin

EssentialContext provides admin functions for managing your trustedForwarder address should you need to change it.

An external function setTrustedForwarder(address newForwarder) is restricted to the contract deployer. An internal function is also available if you have different access control needs for administrative functions. We don't anticipate frequent deploys of the canonical EssentialForwarder contracts, but you may decide to deploy your own for customization purposes.

NFT and Owner Data

When writing your contract that integrates NFT Global Entry you will depend on the functions _msgSender() and _msgNFT() to get data about the caller (NFT owner) and NFT data.

You should not write functions with arguments like tokenId or contractAddress to represent the NFT being "used."

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

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

contract MyContract is EssentialERC2771Context {
    constructor(address trustedForwarder) EssentialERC2771Context(trustedForwarder) {}
    
    function tokenGatedFunction() external onlyForwarder {
        address ownerOrDelegate = _msgSender();
        
        IForwardRequest.NFT memory nft = _msgNFT();
    }
}

_msgSender()

Calls made to your contract's Global Entry functions will include the EssentialForwarder as the msg.sender. You must replace msg.sender with _msgSender() in order to pull the address of the original submitter from calldata.

_msgSender() is guaranteed to own or have authorization to use _msgNFT() any time your function is called. Global Entry supports Account Delegation via Delegate Cash, and allows your frontend to specify the address that your contract will receive as _msgSender().

This means that _msgSender() is not necessarily the address that signed or submitted the transaction, but is always an address that owns or has authorization to use that NFT, and has given the transaction submitter authorization as well.

This allows you to build games with great UX without sacrificing your game or users' security. A user can play a game, using an NFT from a hardware wallet, signing Global Entry transactions from a burner wallet, with game reward tokens going to a hot wallet. As long as the user has created a chain of delegations, transactions that pass a delegation tree walk will be valid.

_msgNFT()

Rather than writing external functions with arguments that represent the NFT being used, your L2 contract will instead use _msgNFT() to get the verified data about an NFT _msgSender() has authority to use.

_msgNFT() returns a struct, IEssentialForwarder.GlobalEntryNFT:

struct GlobalEntryNFT {
    address contractAddress;
    uint256 tokenId;
    uint256 chainId;
}

You can use this data however you need in your function logic to start creating crosschain token-gated applications. Maybe you're writing an onchain raffle with 1 NFT = 1 entry:

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

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

contract MyContract is EssentialERC2771Context {
    mapping(bytes => address) public entries;
    address[] public entrants;
    
    constructor(address trustedForwarder) EssentialERC2771Context(trustedForwarder) {}
    
    function tokenGatedFunction() external onlyForwarder {
        IForwardRequest.NFT memory nft = _msgNFT();
        bytes memory _id = abi.encode(nft.chainId, nft.contractAddress, nft.tokenId);
        
        require(entries[_id] == address(0), "Already Entered");
        
        address ownerOrDelegate = _msgSender();
        entries[_id] = ownerOrDelegate;
        entrants.push(ownerOrDelegate);
    }
}

Security Considerations

NFT Global Entry is only appropriate for applications outside of finance - do not use Global Entry for anything related to loans, staking or anything involving transferring ownership.

When 0xEssential's Ownership Oracle generates a proof of NFT ownership, the proof includes a timestamp. EssentialForwarder verifies this proof, and requires that the timestamp is no more than 10 minutes old. It's important to understand how this can be abused, and to recognize that if you require a tighter grace period you may deploy your own EssentialForwarder and customize that parameter. An NFT holder could in theory generate a proof of their ownership, sell the NFT, and still be able to submit a transaction that passes NFT Global Entry authorization checks for the next 10 minutes.

Ownership proofs also depend on an onchain nonce per proof requester, so ownership proofs become invalid as soon as they are used in an onchain transaction and the requester nonce is incremented.

Testing Your Contract

0xEssential provide utilities to help you write tests in JS/TS with hardhat or in Solidity with foundry.

Next Steps

Once you've inherited EssentialContext and written some logic that depends on a crosschain NFT, you're ready to start working on your client app. 0xEssential offers a client SDK for React apps, @xessential/react and an ethers-based package @xessential/signer for other JS/TS environments.

If you're using React on your frontend, Global Entry is simple to integrate, particularly if you're already using wagmi hooks. Head to our React guide to get started.

If you're not using React for your client app, you can use @xessential/signer and the ethers compatible EssentialSigner class to handle transaction preparation, proof fetching, and transaction sending, whether you're using gasless or standard transactions.

Last updated