Skip to main content
Deploy smart contracts to Immutable zkEVM using Hardhat, a popular Ethereum development environment.
Use Immutable Contract Presets: Immutable provides audited, production-ready contract presets that include built-in royalty enforcement, operator allowlists, and metadata management. See the ERC-721, ERC-1155, and ERC-20 preset documentation for details.

Immutable zkEVM Collection Requirements

All NFT collections deployed on Immutable zkEVM must implement:
  • Royalty Enforcement: EIP-2981 royalty standard with on-chain enforcement via the Operator Allowlist
  • Operator Allowlist: Restrict transfers to approved marketplaces and platforms
  • Metadata Standards: ERC-721/ERC-1155 metadata standards with proper token/collection URIs
Immutable’s preset contracts handle these requirements automatically. If using custom contracts, ensure they meet these standards.
See the Operator Allowlist and Royalties documentation for detailed implementation guidance.

Setup Hardhat

Install and configure Hardhat following the official Hardhat setup guide. Install the toolbox and dotenv packages:
npm install --save-dev @nomicfoundation/hardhat-toolbox dotenv

Configure Hardhat

To deploy contracts to Immutable zkEVM, configure your hardhat.config.ts file with network settings:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";

dotenv.config();

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.19",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    immutableZkevmTestnet: {
      url: "https://rpc.testnet.immutable.com",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
    immutableZkevmMainnet: {
      url: "https://rpc.immutable.com",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
  },
};

export default config;
Network Details:
  • Testnet RPC: https://rpc.testnet.immutable.com (Chain ID: 13473)
  • Mainnet RPC: https://rpc.immutable.com (Chain ID: 13371)
The deployer account must have sufficient IMX for gas fees. Get testnet IMX from the Immutable Testnet Faucet.

Environment Variables

Create a .env file in your project root:
PRIVATE_KEY=your_wallet_private_key_here
Security: Never commit your .env file to version control. Add it to .gitignore:
.env
node_modules/
artifacts/
cache/

Add Contract

After installing the preset contract library (npm install @imtbl/contracts), create a contracts directory and add your contract file.
Solidity Version: The preset contracts use Solidity 0.8.19. Ensure your hardhat.config.ts has version: "0.8.19" in the solidity section.
Operator Allowlist Addresses (required constructor parameter):
NetworkAddress
Testnet0x6b969FD89dE634d8DE3271EbE97734FEFfcd58eE
Mainnet0x1B16F1Da2E5DF531512E15F68c86ac0A7C2a6929
The Operator Allowlist restricts NFT transfers to approved marketplaces. See Operator Allowlist for details.
Create contracts/MyERC721.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@imtbl/contracts/contracts/token/erc721/preset/ImmutableERC721.sol";

contract MyERC721 is ImmutableERC721 {
    constructor(
        address owner_,
        string memory name_,
        string memory symbol_,
        string memory baseURI_,
        string memory contractURI_,
        address operatorAllowlist_,
        address royaltyReceiver_,
        uint96 feeNumerator_
    )
        ImmutableERC721(
            owner_,
            name_,
            symbol_,
            baseURI_,
            contractURI_,
            operatorAllowlist_,
            royaltyReceiver_,
            feeNumerator_
        )
    {}
}

Compile

Compile your contracts to generate artifacts and type definitions:
npx hardhat compile
Output:
Compiling...
Compiled 1 contract successfully
This generates:
  • Contract artifacts in artifacts/
  • ABI files for contract interaction

Test

Write tests in the test/ directory using Hardhat’s testing framework (Mocha and Chai):
import { expect } from "chai";
import { ethers } from "hardhat";

describe("MyERC721", function () {
  it("Should deploy with correct name and symbol", async function () {
    const [owner] = await ethers.getSigners();

    const MyERC721 = await ethers.getContractFactory("MyERC721");
    const contract = await MyERC721.deploy(
      owner.address,
      "My Collection",
      "MYC",
      "https://example.com/metadata/",
      "https://example.com/collection.json",
      "0x6b969FD89dE634d8DE3271EbE97734FEFfcd58eE", // Testnet allowlist
      owner.address,
      200 // 2% royalty
    );

    await contract.waitForDeployment();

    expect(await contract.name()).to.equal("My Collection");
    expect(await contract.symbol()).to.equal("MYC");
  });
});
Run tests:
npx hardhat test
Hardhat automatically compiles contracts before running tests, so npx hardhat compile is not required before testing.

Deploy

Write Deployment Script

Important: Immutable zkEVM requires specific gas configuration. Always include transaction overrides with maxFeePerGas, maxPriorityFeePerGas, and gasLimit. Create scripts/deploy.ts:
import { ethers } from "hardhat";

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying contracts with account:", deployer.address);
  console.log("Account balance:", (await ethers.provider.getBalance(deployer.address)).toString());

  const MyERC721 = await ethers.getContractFactory("MyERC721");

  const contract = await MyERC721.connect(deployer).deploy(
    deployer.address,                           // owner
    "My Collection",                            // name
    "MYC",                                      // symbol
    "https://example.com/metadata/",            // baseURI
    "https://example.com/collection.json",      // contractURI
    "0x6b969FD89dE634d8DE3271EbE97734FEFfcd58eE", // operatorAllowlist (testnet)
    deployer.address,                           // royaltyReceiver
    200,                                        // feeNumerator (2%)
    {
      maxPriorityFeePerGas: 10e9, // 10 gwei
      maxFeePerGas: 15e9, // 15 gwei
      gasLimit: 200000, // Set an appropriate gas limit for your transaction
    }
  );

  await contract.waitForDeployment();
  const address = await contract.getAddress();

  console.log("Contract deployed to:", address);
  console.log("Transaction hash:", contract.deploymentTransaction()?.hash);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Constructor Parameters:
  • owner: Address with admin rights (typically deployer)
  • name: Collection name (e.g., “My NFT Collection”)
  • symbol: Token symbol for ERC-721 (e.g., “MYC”)
  • baseURI: Base URI for token metadata (e.g., “https://api.example.com/metadata/”)
  • contractURI: Collection-level metadata URI
  • operatorAllowlist: Address of the Operator Allowlist contract (see table above)
  • royaltyReceiver: Address that receives royalty payments
  • feeNumerator: Royalty percentage in basis points (200 = 2%, 1000 = 10%)

Deploy to Testnet

Deploy your contract to Immutable zkEVM testnet:
npx hardhat run scripts/deploy.ts --network immutableZkevmTestnet
Expected output:
Deploying contracts with account: 0x1234...
Account balance: 1000000000000000000
Contract deployed to: 0x96dBDB46eCeEFd7082AE6461A83A6f08C8F5cd1C
Transaction hash: 0x5678...
Save the deployed contract address for the next steps.

Deploy to Mainnet

Production Deployment: Test thoroughly on testnet before deploying to mainnet. Ensure:
  • Contract has been audited if using custom logic
  • Deployer account has sufficient IMX for gas fees
  • All parameters (metadata URIs, allowlist address, royalty settings) are correct
  • Mainnet Operator Allowlist address: 0x1B16F1Da2E5DF531512E15F68c86ac0A7C2a6929
npx hardhat run scripts/deploy.ts --network immutableZkevmMainnet

Gas Pricing

Immutable zkEVM enforces a minimum gas price of 10 gwei to protect against spam traffic and ensure efficient transaction processing.

Minimum Requirements

  • Minimum Gas Price: Transactions with a tip cap below 10 gwei are rejected by the RPC
  • Fee Cap: Must be greater than or equal to 10 gwei
  • Base Fee: Transactions only mine when the gas fee cap is greater than or equal to the base fee
The base fee is generally expected to stay below the minimum gas tip cap. When deploying contracts, use the following gas settings:
{
  maxPriorityFeePerGas: 10e9, // 10 gwei (minimum)
  maxFeePerGas: 15e9, // 15 gwei
  gasLimit: 200000, // Set an appropriate gas limit for your transaction
}
Gas Parameters:
  • maxPriorityFeePerGas: Priority fee (tip) you’re willing to pay to miners (minimum 10 gwei)
  • maxFeePerGas: Maximum total fee per gas unit (base fee + priority fee)
  • gasLimit: Maximum gas units the transaction can consume (set higher for contract deployments)
For advanced gas price optimization, use the Ethereum RPC specification eth_feeHistory method to analyze recent gas prices and adjust accordingly.

Troubleshooting

UNPREDICTABLE_GAS_LIMIT Error

Error message:
reason: 'cannot estimate gas; transaction may fail or may require manual gas limit',
code: 'UNPREDICTABLE_GAS_LIMIT',
error: Error: gas required exceeds allowance or always failing transaction
Causes:
  1. Incorrect constructor arguments: Verify all parameters are correct (especially addresses)
  2. Insufficient gas limit: The default limit may be too low for complex contracts
  3. Contract logic error: The contract may revert due to a bug
Solutions:
  1. Verify constructor arguments: Double-check all addresses, strings, and numbers
  2. Estimate and increase gas limit: First estimate the required gas, then set gasLimit accordingly:
    // Estimate gas to determine required limit
    const estimatedGas = await MyERC721.getDeployTransaction(...constructorArgs).then(tx =>
      ethers.provider.estimateGas(tx)
    );
    console.log("Estimated gas:", estimatedGas.toString());
    
    // Set gasLimit higher than estimate (add ~20% buffer)
    const gasOverrides = {
      maxPriorityFeePerGas: 10e9, // 10 gwei
      maxFeePerGas: 15e9, // 15 gwei
      gasLimit: Math.ceil(Number(estimatedGas) * 1.2),
    };
    
  3. Test locally: Run npx hardhat test to catch errors before deployment
After deployment, link your contract via Immutable Hub to enable metadata management, analytics, and Minting API support.

Minting API Prerequisites

If your contract will use the Minting API for programmatic minting, grant the Minting API the required MINTER_ROLE:
// Get the Minting API address from Hub (under Contract > Minting API section)
const MINTING_API_ADDRESS = "0x..."; // From Hub

// Grant minter role
const MINTER_ROLE = await contract.MINTER_ROLE();
await contract.grantRole(MINTER_ROLE, MINTING_API_ADDRESS);

console.log("Minter role granted to Minting API");
Preset Compatibility: The Minting API has been rigorously tested with Immutable’s preset contracts. If using custom contracts, thoroughly test compatibility. Immutable provides no compatibility guarantees for custom implementations.
See the Minting API documentation for complete setup instructions.

Contract Verification

Verify your contract on Immutable Explorer to receive a green checkmark indicating legitimacy.
1

Navigate to contract on Explorer

Go to your contract on Immutable Explorer:
  • Testnet: https://explorer.testnet.immutable.com/address/YOUR_CONTRACT_ADDRESS
  • Mainnet: https://explorer.immutable.com/address/YOUR_CONTRACT_ADDRESS
2

Click 'Verify & Publish'

Select the verification method (via source code or compiler settings).
3

Submit source code and compiler settings

Provide your contract source code and match the compiler settings from your hardhat.config.ts.
4

Receive verification badge

Once verified, your contract displays a green checkmark and users can read the source code directly on Explorer.
Verified contracts allow users to interact with contract functions directly through the Explorer UI, improving transparency and trust.

Next Steps