Set up local contract development
This tutorial will guide you through the steps required to ensure you're ready to go.
Create project
This section uses Hardhat for managing the smart contract development environment. Follow the steps in their tutorial in order to:
- set up your local environment
- create a new Hardhat project
When running npx hardhat init
, you may be presented with the following option:
- Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n)
In order for the tutorials in this section to work, make sure you select yes (y).
Run these commands to install additionally required libraries:
npm install dotenv @ethersproject/bignumber
Alternatives to using Hardhat
Foundry is another popular alternative that can be used to set up your own smart contract development environment.
Add contracts
Immutable zkEVM is fully EVM compatible, which means it can support all Solidity contracts. Additionally, we provide a contracts library which contains a set of recommended smart contract presets.
contracts
Once you have successfully created a Hardhat project, you can add one of our recommended smart contract presets by installing the library as a package inside your smart contract project.
Run this command to install the contracts library:
npm install @imtbl/contracts
You can see a full list of recommended presets here.
Immutable zkEVM collection requirements
All in-game asset collections (i.e. ERC721 and ERC1155) deployed on Immutable's zkEVM must utilise the operator allowlist to protect content creator (eg: you, the game studio's) royalty fees and Immutable's 2% protocol fees.
This can be achieved through embedding the following into your smart contract collection designs:
Recommended Approach
For seamless implementation use or inherit Immutable's preset contracts tailored for gas-efficient creation of Web3 gaming assets on the zkEVM network. Immutable offers the following 3 preset contracts; these contracts implement the Operator Allowlist:
- Recommended Presets for Web3 Games:
- ImmutableERC721.sol - Multiple batch minting functions for optimised gas efficiency. Compatible with Minting API. Multiple batch minting functions for offering gas efficiencies for different Web3 gaming use cases.
- ImmutableERC1155.sol. Semi-fungible tokens. Compatible with Minting API.
- Alternative Presets:
- ImmutableERC721MintByID.sol. Single batch minting function requiring game studio to specify token_id at time of mint. Optimised for gas, yet less gas efficient than ImmutableERC721.sol. Compatible with Minting API.
Minimally Required Standard
If the above options are not compatible with your game design, it is mandatory for your collection to inherit the OperatorAllowlistEnforced.sol contract and adding necessary overrides to approve and transfer functions, allowing your collection to interact with the OperatorAllowlist
. OperatorAllowlistEnforced
includes the modifier functions to identify compliant smart contracts for approvals and transfers, ensuring the preservation of content creators’ royalty fees and protocol fees in third-party marketplace transactions.
We recommend using our presets, if you don’t then you need to implement our Operator Allowlist.
To make sure that creators receive their hard earned loyalties, Immutable requires that all collections of ERC-721/1155s deployed on Immutable zkEVM implement the Operator Allowlist. Not implementing will have these effects for your collection (i.e. ERC-721/1155s):
- You may forfeit any token grants you may have received.
- Passport users will receive a warnings that your collection is likely to be counterfeit.
- Your collection will not appear on any of the 3rd party marketplaces in our ecosystem.
For more information on the Operator Allowlist, please see this guide.
Example contracts using OpenZeppelin's ERC721 and ERC1155 standards with the OperatorAllowlistEnforced
contract are provided below:
- ERC721
- ERC721 Upgradable
- ERC1155
- ERC1155 Upgradable
Below is an example ClashOfCatsERC721
contract that inherits our OperatorAllowlistEnforced
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {OperatorAllowlistEnforced} from '@imtbl/contracts/contracts/allowlist/OperatorAllowlistEnforced.sol';
contract ClashOfCatsERC721 is ERC721, Ownable, OperatorAllowlistEnforced {
constructor(
string memory name,
string memory symbol,
address operatorAllowlist_
) ERC721(name, symbol) Ownable() {
// OAL address is set in the constructor
_setOperatorAllowlistRegistry(operatorAllowlist_);
}
// Overrides _approve function to include `validateApproval` modifier for OAL
function _approve(
address to,
uint256 tokenId
) internal override(ERC721) validateApproval(to) {
super._approve(to, tokenId);
}
// Overrides setApprovalForAll function to include `validateApproval` modifier for OAL
function setApprovalForAll(
address operator,
bool approved
) public override(ERC721) validateApproval(operator) {
super.setApprovalForAll(operator, approved);
}
// Overrides _transfer function to include `validateTransfer` modifier for OAL
function _transfer(
address from,
address to,
uint256 tokenId
) internal override(ERC721) validateTransfer(from, to) {
super._transfer(from, to, tokenId);
}
}
Below is an example ClashOfCatsERC721Upgradable
contract that inherits our OperatorAllowlistEnforced
:
pragma solidity 0.8.19;
import '@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {OperatorAllowlistEnforced} from '@imtbl/contracts/contracts/allowlist/OperatorAllowlistEnforced.sol';
contract ClashOfCatsERC721Upgradable is
ERC721Upgradeable,
OperatorAllowlistEnforced
{
function initialize(
string memory name_,
string memory symbol_,
address operatorAllowlist_
) public initializer {
__ERC721_init(name_, symbol_);
// OAL address is set in the constructor
_setOperatorAllowlistRegistry(operatorAllowlist_);
}
// Overrides approve function to include `validateApproval` modifier for OAL
function approve(
address to,
uint256 tokenId
) public virtual override(ERC721Upgradeable) validateApproval(to) {
super.approve(to, tokenId);
}
// Overrides setApprovalForAll function to include `validateApproval` modifier for OAL
function setApprovalForAll(
address operator,
bool approved
) public virtual override(ERC721Upgradeable) validateApproval(operator) {
super.setApprovalForAll(operator, approved);
}
// Overrides _safeTransfer function to include `validateTransfer` modifier for OAL
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory _data
) internal virtual override(ERC721Upgradeable) validateTransfer(from, to) {
super._safeTransfer(from, to, tokenId, _data);
}
// Overrides transferFrom function to include `validateTransfer` modifier for OAL
function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override(ERC721Upgradeable) validateTransfer(from, to) {
super.transferFrom(from, to, tokenId);
}
}
Below is an example ClashOfCatsERC1155
contract that inherits our OperatorAllowlistEnforced
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import '@openzeppelin/contracts/token/ERC1155/ERC1155.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {OperatorAllowlistEnforced} from '@imtbl/contracts/contracts/allowlist/OperatorAllowlistEnforced.sol';
contract ClashOfCatsERC1155 is ERC1155, Ownable, OperatorAllowlistEnforced {
constructor(
string memory baseTokenURI,
address operatorAllowlist_
) ERC1155(baseTokenURI) Ownable() {
// OAL address is set in the constructor
_setOperatorAllowlistRegistry(operatorAllowlist_);
}
// Overrides _safeTransferFrom function to include `validateTransfer` modifier for OAL
function _safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes memory data
) internal override validateTransfer(from, to) {
super._safeTransferFrom(from, to, id, value, data);
}
// Overrides _safeBatchTransferFrom function to include `validateTransfer` modifier for OAL
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal override validateTransfer(from, to) {
super._safeBatchTransferFrom(from, to, ids, values, data);
}
// Overrides setApprovalForAll function to include `validateApproval` modifier for OAL
function setApprovalForAll(
address operator,
bool approved
) public override(ERC1155) validateApproval(operator) {
super.setApprovalForAll(operator, approved);
}
}
Below is an example ClashOfCatsERC1155Upgradable
contract that inherits our OperatorAllowlistEnforced
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.19;
import '@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
import {OperatorAllowlistEnforced} from './OperatorAllowlistEnforced.sol';
contract ClashOfCatsERC1155Upgradable is
ERC1155Upgradeable,
OperatorAllowlistEnforced
{
function initialize(
string memory uri_,
address operatorAllowlist_
) public initializer {
__ERC1155_init(uri_);
// OAL address is set in the constructor
_setOperatorAllowlistRegistry(operatorAllowlist_);
}
// Overrides _safeTransferFrom function to include `validateTransfer` modifier for OAL
function _safeTransferFrom(
address from,
address to,
uint256 id,
uint256 value,
bytes memory data
) internal override(ERC1155Upgradeable) validateTransfer(from, to) {
super._safeTransferFrom(from, to, id, value, data);
}
// Overrides _safeBatchTransferFrom function to include `validateTransfer` modifier for OAL
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
) internal override(ERC1155Upgradeable) validateTransfer(from, to) {
super._safeBatchTransferFrom(from, to, ids, values, data);
}
// Overrides setApprovalForAll function to include `validateApproval` modifier for OAL
function setApprovalForAll(
address operator,
bool approved
) public override(ERC1155Upgradeable) validateApproval(operator) {
super.setApprovalForAll(operator, approved);
}
}
Here is a simple collection operator allowlist checklist you should follow to ensure your game has no delays at launch:
- Have you imported
OperatorAllowlistEnforced.sol
? - Have you implemented the allowlist check on transfers?
- Have you implemented the allowlist check on approvals?
The use of the preset contracts accelerates integration with Immutable's ecosystem. If your studio has developed custom ERC721 or ERC1155 collections, ensure they adhere to their respective standards in addition to inheriting the OperatorAllowlistEnforced.sol contract, to safeguard compatibility with Immutable's ecosystem.