# 8 Common NFT Smart Contract Mistakes Engineers Make (and How to Fix Them)

> From reentrancy to metadata mutability, these are the NFT smart contract mistakes that end up costing projects money, trust, and sometimes the ability to operate.

- **Published:** 2026-05-11
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** nft, smart-contracts, solidity, security, common-mistakes
- **Canonical URL:** https://clixo.sh/blog/nft-smart-contract-common-mistakes

Most NFT contract bugs do not look like bugs until they are being exploited or until a project can no longer operate the way it promised. The mistakes below appear repeatedly across audits, post-mortems, and community support channels. None of them are exotic. All of them are avoidable.

## Mistake 1: No Reentrancy Protection on Mint Functions

If your mint function sends ETH (for refunds) or calls external contracts before updating state, it is vulnerable to reentrancy. An attacker can re-enter your mint function before the first call completes and mint more tokens than they paid for.

**Fix**: Follow the checks-effects-interactions pattern. Update all state (token counts, balances, mappings) before making any external call or transfer. For additional protection, use OpenZeppelin's `ReentrancyGuard` modifier on functions that involve ETH transfers.

## Mistake 2: Unbounded Loops Over Token Supply

A function that iterates over all minted tokens — to compute rarity, check ownership, or calculate rewards — will eventually run out of gas as the collection grows. If the function is required for normal operation, your contract becomes unusable at scale.

**Fix**: Never iterate over token supply in a transaction. Use mappings and incremental state updates instead. If you need aggregate data, index it off-chain using an event listener.

## Mistake 3: Mutable Metadata URI With No Lock

Setting a `baseURI` is common. What many projects forget is that if the owner can call `setBaseURI` at any time forever, collectors have no guarantee that the metadata they bought will remain the metadata they hold. This is a rug vector — even if unintentional.

**Fix**: Build a `lockMetadata()` function that permanently disables `setBaseURI`. Call it after reveal. Emit an event so the state change is publicly verifiable. Collectors should be able to confirm on-chain that metadata is frozen.

## Mistake 4: Weak Randomness for Reveal or Trait Assignment

Using `block.timestamp`, `block.difficulty`, or `blockhash` as a randomness source is not secure. Validators can influence these values. Miners used to front-run this for profitable trait assignments, and the same risk exists today.

**Fix**: Use Chainlink VRF (Verifiable Random Function) for any randomness that affects token value. It is an external call and adds complexity, but it is the only credible source of verifiable randomness for trait assignment and reveal mechanics.

## Mistake 5: Missing Access Control on Sensitive Functions

A `withdraw()` function anyone can call. A `mint()` with no `onlyOwner` guard. A `setRoyalty()` that accepts any caller. These are not hypotheticals — they appear in audited codebases.

**Fix**: Use OpenZeppelin's `Ownable` or `AccessControl` on every function that changes state, withdraws funds, or alters configuration. Review each public function and ask: who should be allowed to call this?

## Mistake 6: Integer Overflow on Supply Caps

Solidity 0.8+ has built-in overflow protection, but projects using older compilers or upgrading legacy code may still have unchecked arithmetic. If your mint count can overflow, an attacker can wrap it around and mint beyond your supply cap.

**Fix**: Use Solidity 0.8.x or later. If you use `unchecked` blocks for gas optimization, be explicit about which arithmetic is safe to leave unchecked and document why.

## Mistake 7: Allowing ETH to Get Stuck

A contract that receives ETH but has no `withdraw` function — or has a broken one — will lock funds permanently. Contracts also get ETH sent to them accidentally. If there is no recovery path, that ETH is gone.

**Fix**: Include a `withdraw()` function with appropriate access control. Test it. If your contract accepts ETH, test that `withdraw` correctly drains the full balance, including edge cases around partial mints and refunds.

## Mistake 8: Signature Replay in Allowlist Minting

Allowlist mints typically use signed messages to authorize specific addresses. If the signature is not tied to the specific contract address and chain ID, the same signature can be replayed on a different deployment — or on a different chain where you have deployed a copy.

**Fix**: Include the contract address and chain ID in the signed message. Use EIP-712 typed data signing, which handles this structurally. OpenZeppelin's `EIP712` base contract makes this straightforward and produces human-readable wallet prompts.

---

```mermaid
flowchart LR
  A["Write Contract"] --> B["Slither static analysis"]
  B -->|"Issues found"| A
  B --> C["Unit tests — all functions including edge cases"]
  C -->|"Failures"| A
  C --> D["Deploy to testnet — full user flow"]
  D --> E["Independent Solidity audit"]
  E -->|"Findings"| A
  E --> F["Mainnet deploy"]
```

## The Pattern Behind All of These Mistakes

Most of these bugs share a common root: the contract was written to make the happy path work and the error paths were not examined. Security in Solidity is adversarial. Assume every public function will be called with adversarial inputs, maximum values, and reentrancy attempts.

A few practices that catch most of this before deployment:

- Write tests for every function, including failure cases and edge cases.
- Run your contract through Slither or another static analysis tool before audit.
- Get an independent audit from a firm that specializes in Solidity — not a generalist developer doing a "review."
- Deploy to a testnet and run through the entire user flow before mainnet.

Smart contract bugs on mainnet are not patches — they are post-mortems. Ship it right the first time.

If you want your NFT contract reviewed or built with these patterns embedded from the start, [talk to the Clixo team](https://clixo.sh/#contact). We build production-grade NFT contracts and can audit contracts you have already written.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
