Custom Contracts With Gasless Transactions & EOA Support

Deploy your own smart contract on any MetaFab supported chain while fully supporting gasless transactions for your players, frictionless gasless transactions for EOA wallets connected to player accounts, and more.

Overview

Oftentimes there's functionality that MetaFab may not support out of the box through our prebuilt contracts.

In these more advanced situations, you will probably want to write and deploy your own contracts to the blockchain while still allowing your players to gaslessly interact with them or make use of interactions with your custom contract through MetaFab's API interface.

You're in luck! This is pretty straight forward to do with MetaFab. We'll walk you through it.

Guide

Step 1: Get ERC2771Context_Upgradeable.sol

You'll want to make a copy of our ERC2771Context_Upgradeable contract below. Alternatively you can find it on our Contracts GitHub repository here.

Add the contract to your project directory as a file named ERC2771Context_Upgradeable.sol.

// SPDX-License-Identifier: Commons-Clause-1.0
//  __  __     _        ___     _
// |  \/  |___| |_ __ _| __|_ _| |__
// | |\/| / -_)  _/ _` | _/ _` | '_ \
// |_|  |_\___|\__\__,_|_|\__,_|_.__/
//
// Launch your crypto game or gamefi project's blockchain
// infrastructure & game APIs fast with https://trymetafab.com

pragma solidity ^0.8.16;

import "@openzeppelin/contracts/utils/Context.sol";

/**
 * @dev Context variant with ERC2771 support and upgradeable trusted forwarder.
 */

abstract contract ERC2771Context_Upgradeable is Context {
  address private trustedForwarder;

  constructor(address _trustedForwarder) {
    trustedForwarder = _trustedForwarder;
  }

  function isTrustedForwarder(address forwarder) public view virtual returns (bool) {
    return forwarder == trustedForwarder;
  }

  function _upgradeTrustedForwarder(address _trustedForwarder) internal {
    trustedForwarder = _trustedForwarder;
  }

  function _msgSender() internal view virtual override returns (address sender) {
    if (isTrustedForwarder(msg.sender)) {
      // The assembly code is more direct than the Solidity version using `abi.decode`.
      /// @solidity memory-safe-assembly
      assembly {
        sender := shr(96, calldataload(sub(calldatasize(), 20)))
      }
    } else {
      return super._msgSender();
    }
  }

  function _msgData() internal view virtual override returns (bytes calldata) {
    if (isTrustedForwarder(msg.sender)) {
      return msg.data[:msg.data.length - 20];
    } else {
      return super._msgData();
    }
  }
}

Step 2: Implement ERC2771Context_Upgradeable.sol

Next you'll need to implement ERC2771Context_Upgradeable.sol into your contract.

Below is an example of how to do this. We'll be using an example contract called MyContract.

❗️

Use _msgSender() instead of msg.sender

Please note, that any logic where you need to get the correct sender address, make sure to use the _msgSender() function instead of using msg.sender. In the case of gasless transactions, msg.sender will be the forwarding contract's address, but _msgSender() will always be the correct and expected sender for both gasless and non-gasless transactions.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.16;

// Your other imports here....

import "@openzeppelin/contracts/access/Ownable.sol";
import "./ERC2771Context_Upgradeable.sol";

contract MyContract is ERC2771Context_Upgradeable, Ownable /* etc.. etc.. */ {
  constructor(address _forwarder /* other constructor args... */)
  ERC2771Context_Upgradeable(_forwarder) {
    /* Any other initialization you may want to do here... */
  }
  
  // Implement your contract's other functions here...

  /**
   * @dev Support for gasless transactions
   * The functions below must be implemented exactly as they are for gasless transactions
   * to work correctly.
   */

  function upgradeTrustedForwarder(address _newTrustedForwarder) external onlyOwner {
    _upgradeTrustedForwarder(_newTrustedForwarder);
  }

  function _msgSender() internal view override(Context, ERC2771Context_Upgradeable) returns (address) {
    return super._msgSender();
  }

  function _msgData() internal view override(Context, ERC2771Context_Upgradeable) returns (bytes calldata) {
    return super._msgData();
  }
}

Step 3: Get System.sol and ISystem.sol

You'll want to make a copy of our System contract and ISystem interface below. Alternatively you can find System.sol contract here and the ISystem.sol contract interface here.

Add the contract to your project directory as a file named System.sol and the interface as ISystem.sol.

// SPDX-License-Identifier: Commons-Clause-1.0
//  __  __     _        ___     _
// |  \/  |___| |_ __ _| __|_ _| |__
// | |\/| / -_)  _/ _` | _/ _` | '_ \
// |_|  |_\___|\__\__,_|_|\__,_|_.__/
//
// Launch your crypto game or gamefi project's blockchain
// infrastructure & game APIs fast with https://trymetafab.com

pragma solidity ^0.8.16;

import "./ISystem.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

abstract contract System is ISystem {
  using EnumerableSet for EnumerableSet.Bytes32Set;
  EnumerableSet.Bytes32Set private systemIds;
  bytes32 private initializedSystemId;

  constructor(bytes32 _systemId) {
    systemIds.add(_systemId);
    initializedSystemId = _systemId;
  }

  function addSystemId(bytes32 _systemId) external {
    systemIds.add(_systemId);
  }

  function removeSystemId(bytes32 _systemId) external {
    systemIds.remove(_systemId);
  }

  function systemId() public view returns (bytes32) { // returns the initialized systemId (legacy)
    return initializedSystemId;
  }

  function supportsSystemId(bytes32 _systemId) public view returns (bool) {
    return systemIds.contains(_systemId);
  }

  function supportedSystemIds() public view returns (bytes32[] memory) {
    return systemIds.values();
  }
}
// SPDX-License-Identifier: Commons-Clause-1.0
//  __  __     _        ___     _
// |  \/  |___| |_ __ _| __|_ _| |__
// | |\/| / -_)  _/ _` | _/ _` | '_ \
// |_|  |_\___|\__\__,_|_|\__,_|_.__/
//
// Launch your crypto game or gamefi project's blockchain
// infrastructure & game APIs fast with https://trymetafab.com

pragma solidity ^0.8.16;

interface ISystem {
  function addSystemId(bytes32 _systemId) external;
  function removeSystemId(bytes32 _systemId) external;
  function systemId() external view returns (bytes32); // legacy, initialized systemId
  function supportsSystemId(bytes32 _systemId) external view returns (bool);
  function supportedSystemIds() external view returns (bytes32[] memory);
}

Step 4: Implementing System.sol

Next you'll need to implement System.sol into your contract.

System.sol defines functions used to properly permission internal MetaFab systems for connected EOA wallets for player accounts.

Below is an example building on the previous steps, showing how our example MyContract looks after properly implementing System.sol.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.16;

// Your other imports here....

import "@openzeppelin/contracts/access/Ownable.sol";
import "./ERC2771Context_Upgradeable.sol";
import "./System.sol

contract MyContract is ERC2771Context_Upgradeable, System, Ownable /* etc.. etc.. */ {
  constructor(address _forwarder, bytes32 _systemId /* other constructor args... */)
  ERC2771Context_Upgradeable(_forwarder)
  System(_systemId) {
    /* Any other initialization you may want to do here... */
  }
  
  // Implement your contract's other functions here...

  /**
   * @dev Support for gasless transactions
   * The functions below must be implemented exactly as they are for gasless transactions
   * to work correctly.
   */

  function upgradeTrustedForwarder(address _newTrustedForwarder) external onlyOwner {
    _upgradeTrustedForwarder(_newTrustedForwarder);
  }

  function _msgSender() internal view override(Context, ERC2771Context_Upgradeable) returns (address) {
    return super._msgSender();
  }

  function _msgData() internal view override(Context, ERC2771Context_Upgradeable) returns (bytes calldata) {
    return super._msgData();
  }
}

Step 5: Deploying Your Custom Contract

Once you've written your custom contract and have implemented ERC2771Context_Upgradeable as outlined in the previous step, you're ready to deploy your contract.

Contract deployment is outside of the scope of this guide - if you're unsure how to deploy your contract, look into tools like Hardhat or Remix. MetaFab will support direct custom contract deployment in the future, but does not at this time.

We'll need to pass the correct address _forwarder and bytes32 _systemId constructor arguments when deploying our contract for MetaFab's gasless transactions and EOA wallet support to work. See below for the correct arguments to use for these 2 constructor arguments.

Setting the correct _forwarder constructor argument

You'll need the proper forwarder address for your contract's _forwarder constructor argument while deploying. You can find the latest forwarder addresses based on the chain you're deploying to, below.

ChainMetaFab Forwarder Contract Address
ETHEREUM0x67652e376fe4E2530d6b3432475648886DA7BdA9
GOERLI (Ethereum Testnet)0x67652e376fe4E2530d6b3432475648886DA7BdA9
MATIC0x67652e376fe4E2530d6b3432475648886DA7BdA9
MATICMUMBAI (Polygon Testnet)0x67652e376fe4E2530d6b3432475648886DA7BdA9
ARBITRUM0x67652e376fe4E2530d6b3432475648886DA7BdA9
ARBITRUMNOVA0x67652e376fe4E2530d6b3432475648886DA7BdA9
ARBITRUMGOERLI (Arbitrum Testnet)0x67652e376fe4E2530d6b3432475648886DA7BdA9
AVALANCHE0x67652e376fe4E2530d6b3432475648886DA7BdA9
AVALANCHEFUJI (Avalanche Testnet)0x67652e376fe4E2530d6b3432475648886DA7BdA9
BINANCE0x67652e376fe4E2530d6b3432475648886DA7BdA9
BINANCETESTNET0x67652e376fe4E2530d6b3432475648886DA7BdA9
MOONBEAM0x67652e376fe4E2530d6b3432475648886DA7BdA9
MOONBEAMTESTNET (Moonbase Testnet)0x67652e376fe4E2530d6b3432475648886DA7BdA9
FANTOM0x67652e376fe4E2530d6b3432475648886DA7BdA9
FANTOMTESTNET0x67652e376fe4E2530d6b3432475648886DA7BdA9
THUNDERCORE0x67652e376fe4E2530d6b3432475648886DA7BdA9
THUNDERCORETESTNET0x67652e376fe4E2530d6b3432475648886DA7BdA9

Setting the correct _systemId constructor argument

Additionally, you'll need to provide the correct bytes32 _systemId as a constructor argument. This can be retrieved using an online keccak256 hashing tool like this one. For the input of the keccak256 hash function, use your game's id. Your game's id must be used, otherwise player transactions using connected EOA wallet will fail.

Step 6: Connecting your Deployed Contract to your MetaFab Game

Lastly, head over to our Create Contract endpoint. This endpoint allow you to create a new custom contract connection for your game on MetaFab, allowing interaction with the contract by you and your players through MetaFab APIs. Doing so will automatically enable our players to gaslessly transact with our newly deployed contract.

This endpoint requires the following 3 arguments passed to the request body.

  • address: This is the contract address of your newly deployed custom contract on the blockchain.
  • abi: This is the ABI generated by you for your custom contract. It should be a JSON string.
  • chain: This is the chain you deployed your custom contract to.

Upon successfully submitting your API request, you're receive a response containing a Contract object. This object will include a contract id which can be used throughout the MetaFab APIs to interact with your contract through endpoints like Read Contract and Write Contract.

That's it! Your newly deployed contract is connected to your MetaFab game and can be completely interacted with through MetaFab's APIs and gaslessly interacted with through your player's accounts!