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.
Never commit .env files to version control. Add .env and .env.local to your .gitignore file to prevent accidentally exposing private keys or API secrets.
The Operator Allowlist restricts NFT transfers to approved marketplaces. For details on how it works and how to manage the allowlist, see Operator Allowlist.
Important: Immutable zkEVM requires specific gas configuration. Always include transaction overrides with maxFeePerGas, maxPriorityFeePerGas, and gasLimit.Create scripts/deploy.ts:
Deploy ERC-721 Preset
Deploy ERC-1155 Preset
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); });
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 specificationeth_feeHistory method to analyze recent gas prices and adjust accordingly.
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:
Incorrect constructor arguments: Verify all parameters are correct (especially addresses)
Insufficient gas limit: The default limit may be too low for complex contracts
Contract logic error: The contract may revert due to a bug
Solutions:
Verify constructor arguments: Double-check all addresses, strings, and numbers
Estimate and increase gas limit: First estimate the required gas, then set gasLimit accordingly:
// Estimate gas to determine required limitconst 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),};
Test locally: Run npx hardhat test to catch errors before deployment
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 roleconst 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.