EssentialContext

Inherit EssentialContext in your L2 contract

EssentialContext is the contract primitive you must inherit in your Layer 2 contract to enable Gasless Transactions. If you've used meta-transactions in the past you will be familiar with this process. EssentialContext is compatible with other implementations like OpenZeppelin's Context, so if you already use that contract you may be able to skip this. The important thing to know when writing contracts for Gasless Transactions is that you cannot use msg.sender in your external or public functions. You must replace these calls with _msgSender().

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:

Constructor

EssentialContext must be initialized in your constructor with the address of the EssentialForwarder you're using. If you're using 0xEssential's canonical EssentialForwarder you can use this address across every network we support: 0x000000000066b3aED7Ae8263588dA67fF381FfCa.

//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
    }
}

Specifying the trustedForwarder restricts Gasless Transactions to forwarding contracts you trust. Without this restriction your contract could be manipulated by malicious forwarders.

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 check 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 takes QA and testing seriously. We 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 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