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 usingmsg.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
_forwarder
constructor argumentYou'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.
Chain | MetaFab Forwarder Contract Address |
---|---|
ETHEREUM | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
GOERLI (Ethereum Testnet) | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
MATIC | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
MATICMUMBAI (Polygon Testnet) | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
ARBITRUM | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
ARBITRUMNOVA | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
ARBITRUMGOERLI (Arbitrum Testnet) | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
AVALANCHE | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
AVALANCHEFUJI (Avalanche Testnet) | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
BINANCE | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
BINANCETESTNET | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
MOONBEAM | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
MOONBEAMTESTNET (Moonbase Testnet) | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
FANTOM | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
FANTOMTESTNET | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
THUNDERCORE | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
THUNDERCORETESTNET | 0x67652e376fe4E2530d6b3432475648886DA7BdA9 |
Setting the correct _systemId
constructor argument
_systemId
constructor argumentAdditionally, 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!
Updated 7 months ago