An NFT smart contract is usually a few hundred lines of code on top of an audited library, and those lines handle payments, supply caps and permanent ownership records. This guide covers the choice between ERC-721 and ERC-1155, the common mint patterns, the bugs behind most NFT exploits, and how to test and audit a contract before launch.
The NFT token development guide compares token standards in more depth. This page covers writing and shipping the contract.
What an NFT smart contract should and should not do
The contract records who owns each token, handles transfers and enforces the minting rules. It replaces a central registry, so nobody can reassign a token without the owner, and anyone can check every transfer.
Game logic, search, pricing and most business rules belong off-chain, where code can change without moving tokens to a new contract. Keep the on-chain part small and predictable, with as few special cases as possible.
ERC-721 and ERC-1155 smart contracts compared
| Feature | ERC-721 | ERC-1155 |
|---|---|---|
| Tokens per contract | Each token ID is unique and has one owner | Each token ID can have one copy or many copies |
| Typical use | 1/1 art, PFP collections, domain names | Game items, editions, tickets, badges |
| Batch transfers | Not in the standard; one transfer per token | Built in, so one call can move several token IDs |
| Receiver callback | onERC721Received on safe transfers | onERC1155Received or onERC1155BatchReceived on every transfer to a contract |
| Marketplace support | Supported everywhere | Supported by major marketplaces |
An ERC721 smart contract fits a collection where every item is different. An ERC-1155 contract fits items that come in quantities, such as 500 copies of one sword in a game. ERC-721A, an implementation from the Azuki team, keeps the ERC-721 interface and lowers gas when one wallet mints many tokens in a single transaction.
Start from audited libraries
Use OpenZeppelin Contracts or a library with a similar review record for ERC-721, ERC-1155, access control and EIP-2981 royalties. In October 2026 the current OpenZeppelin release is version 5.7. Version 5 replaced the old transfer hooks with a single _update function, so code written for version 4 needs changes before it compiles. Version 5.6 changed ERC-1155 so that a batch transfer with one item calls the batch receiver hook. Do not write your own transfer or approval logic, because small mistakes in that logic are easy to miss.
Mint patterns
Public mint
A public mint has a fixed price, a cap per wallet and a cap on total supply, and every mint path must respect the total cap, including owner mints and airdrops.
Merkle allowlist
The contract stores one Merkle root, and each allowlisted wallet submits a proof when it mints. The root costs little to store and to update, and each leaf should hold the wallet address and the allowed quantity.
Signature mint
A backend server signs an EIP-712 message that lets one wallet mint a set quantity before a deadline. This pattern is flexible, but the signing key becomes a critical secret. Add a nonce or record each used signature so nobody can replay it, and include the chain ID and the contract address in the signed data.
Lazy mint
A creator signs a voucher that describes the token, and the buyer's transaction mints the token and pays the creator in one step. Creator platforms use this pattern often. See NFT minting platform development for the product side.
A minimal allowlist mint looks like this:
function allowlistMint(uint256 qty, uint256 maxQty, bytes32[] calldata proof)
external payable
{
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, maxQty))));
require(MerkleProof.verify(proof, merkleRoot, leaf), "not allowlisted");
require(minted[msg.sender] + qty <= maxQty, "over allowance");
require(totalSupply() + qty <= MAX_SUPPLY, "sold out");
require(msg.value == qty * price, "wrong payment");
minted[msg.sender] += qty;
_mint(msg.sender, qty);
}
The function checks the proof, the wallet's allowance, the total supply and the payment. It updates the wallet's count before it mints.
Common vulnerabilities
- Reentrancy through safe-mint callbacks.
_safeMintcalls the receiver'sonERC721Receivedfunction, and that function can call your mint function again. Update counters before the mint, or add a reentrancy guard. - Weak randomness. If the contract uses block values to pick traits or reveal offsets, validators or bots can influence the result. Use a commit-reveal scheme or a verifiable source such as Chainlink VRF.
- Signature replay. A signed message without a nonce, chain ID or contract address can be used again on another chain or contract.
- Broken withdrawals. Funds get stuck when the withdraw function sends ETH to a contract that rejects it, and get drained when anyone can call the withdraw function.
- Admins with too much power. One key that can mint without limit, change all metadata and take the funds puts every holder at risk. Split the roles and hold them in a multisig wallet.
- Boundary errors. An off-by-one supply check lets one extra token through.
Royalties and transfer restrictions
EIP-2981 lets a contract report a royalty, but it does not force anyone to pay it. In 2022 and 2023 some collections blocked transfers through marketplaces that skipped royalties, using OpenSea's operator filter registry. OpenSea retired that filter in August 2023.
Limit Break's ERC721-C and ERC1155-C standards enforce royalties in the token contract itself. OpenSea enforces creator earnings for collections that use these standards, and for collections created in OpenSea Studio after April 2, 2024. On OpenSea, enforced collections can trade only on OpenSea and on marketplaces that use Limit Break's Payment Processor, and OpenSea caps creator earnings at 10%. Transfer limits suit tickets and credentials, where the issuer has a reason to control resale, but on art collections they annoy collectors who use other venues.
Upgradeability
A proxy contract lets the team fix bugs, and it also lets the team change the rules after people buy. Many collectible projects ship a token contract that cannot change and put logic that may change in separate minter or utility contracts. If you use a proxy, put the upgrade key behind a multisig and a timelock, and publish that setup.
Testing and audit
- Write unit tests for every mint path, cap and payment calculation.
- Write fuzz tests for quantities, prices and allowlist proofs; Foundry runs fuzz tests with little setup.
- Write invariant tests. Total minted must never exceed the maximum supply, and the contract balance must equal payments minus withdrawals.
- Rehearse the full launch on a mainnet fork, including the reveal and the withdrawal.
- Order an external audit and have the auditor review each fix. See smart contract audits and why audits matter.
- Verify the source code on the block explorer when you deploy.
For Solidity work beyond NFTs, see Ethereum smart contract development.
Frequently asked questions
What is an NFT smart contract?
An NFT smart contract is a program on a blockchain that records who owns each token in a collection, moves tokens between wallets and enforces the minting rules. Most NFT contracts follow the ERC-721 or ERC-1155 standard.
Should I use ERC-721 or ERC-1155?
Use ERC-721 when every item is unique, and ERC-1155 when items come in quantities or when you need to move many items in one transaction.
Do NFT contracts need an audit?
Any contract with custom mint, payment or withdraw logic needs a review. A standard library contract with no custom code that moves value carries less risk, and a short review still helps.
Should my NFT contract be upgradeable?
For collectibles, usually not, because collectors want rules that cannot change. For game items or tickets with changing rules, an upgradeable contract behind a multisig and a timelock can make sense.
Which tools do NFT smart contract developers use?
Most developers build and test with Foundry or Hardhat, and add OpenZeppelin libraries, static analyzers such as Slither, and a Safe multisig for admin keys.