← Back to Blog

NFT Smart Contract Security: ERC-721 and ERC-1155 Vulnerabilities

2026-04-17 solidity security nft erc721 erc1155 reentrancy opensea royalties 2026

NFT contracts have a specific vulnerability profile that differs from DeFi protocols. The risks aren't just financial exploits — they include royalty theft, ownership manipulation, metadata spoofing, and mint mechanics abuse. Many high-profile NFT projects have been compromised through patterns that standard smart contract audits miss because they're NFT-specific.

This post covers the attack surface unique to ERC-721 and ERC-1155 contracts.


ERC-721 safeTransferFrom Reentrancy

The most overlooked NFT vulnerability: safeTransferFrom calls onERC721Received on the recipient if it's a contract. This is a callback — and callbacks enable reentrancy.

// VULNERABLE: external call before state update
contract VulnerableMint {
    uint256 public totalMinted;
    uint256 public constant MAX_SUPPLY = 10000;
    uint256 public constant PRICE = 0.08 ether;
    mapping(uint256 => address) public ownerOf;

    function mint(uint256 quantity) external payable {
        require(msg.value == PRICE * quantity);
        require(totalMinted + quantity <= MAX_SUPPLY);

        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = totalMinted;
            totalMinted++;
            ownerOf[tokenId] = msg.sender;
            // safeTransferFrom internally calls onERC721Received on recipient
            _safeMint(msg.sender, tokenId);  // ← reentrant callback here
        }
    }
}
// Attacker contract
contract MintReentrancy {
    VulnerableMint target;
    uint256 reentrancyCount;

    function attack() external payable {
        target.mint{value: 0.08 ether}(1);
    }

    // Called during _safeMint → triggers another mint
    function onERC721Received(address, address, uint256, bytes calldata)
        external returns (bytes4)
    {
        if (reentrancyCount < 5) {
            reentrancyCount++;
            target.mint{value: 0.08 ether}(1);  // re-enter mint
        }
        return this.onERC721Received.selector;
    }
}

In this attack, the reentrancy allows the attacker to mint multiple tokens before totalMinted is updated beyond the first token — effectively minting at a manipulated supply count, potentially bypassing mint limits.

Fix: checks-effects-interactions + reentrancy guard

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SafeMint is ERC721, ReentrancyGuard {
    uint256 public totalMinted;

    function mint(uint256 quantity) external payable nonReentrant {
        require(msg.value == PRICE * quantity);
        uint256 start = totalMinted;
        totalMinted += quantity;  // update state BEFORE external calls
        require(totalMinted <= MAX_SUPPLY);

        for (uint256 i = 0; i < quantity; i++) {
            _safeMint(msg.sender, start + i);
        }
    }
}

Royalty Bypass

ERC-2981 defines an on-chain royalty standard, but it's advisory — marketplaces aren't required to honor it, and nothing in the token contract enforces payment.

// This does NOT enforce royalties
contract MyNFT is ERC721, ERC2981 {
    constructor() {
        _setDefaultRoyalty(creator, 500); // 5% — advisory only
    }
}

Any marketplace can call transferFrom directly and route zero royalties to the creator. This has been widely exploited — "royalty-free" marketplace aggregators route trades through contracts that bypass royaltyInfo() entirely.

Enforcement approaches and their trade-offs:

// Approach 1: Block non-approved operators (most restrictive)
contract RoyaltyEnforced is ERC721 {
    mapping(address => bool) public approvedOperators;

    function approve(address to, uint256 tokenId) public override {
        require(approvedOperators[to], "Operator not approved");
        super.approve(to, tokenId);
    }

    function setApprovalForAll(address operator, bool approved) public override {
        require(!approved || approvedOperators[operator], "Operator not approved");
        super.setApprovalForAll(operator, approved);
    }
}

Trade-off: restricting operators reduces liquidity. Users can't list on new marketplaces until the contract owner adds them. OpenSea and other marketplaces initially fought this approach, creating a royalty wars standoff.

Practical reality: On-chain royalty enforcement is a policy decision, not just a security one. Many projects have moved to off-chain enforcement layers (e.g., EIP-4910, operator-filter-registry) or accepted that royalties are voluntary.


Metadata Manipulation

// VULNERABLE: centralized metadata
contract MutableNFT is ERC721 {
    string private _baseURIValue = "https://api.myproject.com/metadata/";

    function setBaseURI(string memory newURI) external onlyOwner {
        _baseURIValue = newURI;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        return string(abi.encodePacked(_baseURIValue, tokenId.toString()));
    }
}

Problems with centralized metadata:
1. Rug pull vector: Owner can change _baseURIValue to point to different images/attributes — all token metadata changes overnight
2. Censorship risk: API server can go offline or geo-restrict
3. Value destruction: NFT buyers assume the image/attributes are permanent; they're not

Fix: IPFS or on-chain metadata, emit events on changes

// Option 1: Immutable IPFS base URI (set once, never changed)
contract ImmutableNFT is ERC721 {
    string private immutable _baseURIValue;

    constructor(string memory baseURI) {
        _baseURIValue = baseURI;  // ipfs://Qm... — set once at deploy
    }

    // No setBaseURI function
}

// Option 2: Emit event on metadata changes for transparency
event MetadataUpdate(uint256 tokenId);
event BatchMetadataUpdate(uint256 fromTokenId, uint256 toTokenId);

function setBaseURI(string memory newURI) external onlyOwner {
    _baseURIValue = newURI;
    emit BatchMetadataUpdate(0, type(uint256).max);  // EIP-4906 standard event
}

Signature Minting Exploits

Many NFT projects use off-chain allowlist signatures to gate minting:

// VULNERABLE: signature reuse across chains / contract versions
contract AllowlistMint is ERC721 {
    address public signer;
    mapping(bytes32 => bool) public usedSignatures;

    function mintWithSignature(
        address recipient,
        uint256 quantity,
        bytes calldata signature
    ) external {
        bytes32 hash = keccak256(abi.encodePacked(recipient, quantity));
        bytes32 ethHash = hash.toEthSignedMessageHash();
        require(ethHash.recover(signature) == signer, "Invalid signature");

        bytes32 sigHash = keccak256(signature);
        require(!usedSignatures[sigHash], "Already used");
        usedSignatures[sigHash] = true;

        _mint(recipient, quantity);
    }
}

Vulnerabilities:
1. Cross-chain replay: Same signature works on Ethereum and Polygon if the contract is deployed at the same address on both chains — the hash doesn't include block.chainid
2. Cross-contract replay: If a V2 contract is deployed at the same address with the same signer, old signatures are valid again
3. Parameter manipulation: The hash doesn't include a nonce or expiry — a valid signature never expires

Fix: EIP-712 structured signing with chain ID and contract address

contract SafeAllowlistMint is EIP712, ERC721 {
    bytes32 private constant MINT_TYPEHASH = keccak256(
        "MintRequest(address recipient,uint256 quantity,uint256 nonce,uint256 expiry)"
    );
    mapping(address => uint256) public nonces;

    function mintWithSignature(
        address recipient,
        uint256 quantity,
        uint256 expiry,
        bytes calldata signature
    ) external {
        require(block.timestamp <= expiry, "Signature expired");

        bytes32 structHash = keccak256(abi.encode(
            MINT_TYPEHASH,
            recipient,
            quantity,
            nonces[recipient]++,
            expiry
        ));

        address recovered = _hashTypedDataV4(structHash).recover(signature);
        require(recovered == signer, "Invalid signature");

        _mint(recipient, quantity);
    }
}

_hashTypedDataV4 includes the domain separator which encodes block.chainid and address(this) — making signatures chain-specific and contract-specific.


ERC-1155 Batch Transfer Reentrancy

ERC-1155's safeBatchTransferFrom triggers onERC1155BatchReceived on recipient contracts. Combined with batch semantics, this enables more complex reentrancy than ERC-721:

// VULNERABLE: ERC-1155 marketplace with reentrancy window
contract NFTMarketplace {
    mapping(uint256 => Listing) public listings;

    function buyBatch(uint256[] calldata tokenIds) external payable {
        uint256 totalPrice;
        for (uint256 i = 0; i < tokenIds.length; i++) {
            totalPrice += listings[tokenIds[i]].price;
        }
        require(msg.value >= totalPrice);

        // Transfer all tokens — triggers onERC1155BatchReceived
        nftContract.safeBatchTransferFrom(
            address(this),
            msg.sender,
            tokenIds,
            quantities,
            ""
        );

        // VULNERABLE: state cleared AFTER external call
        for (uint256 i = 0; i < tokenIds.length; i++) {
            delete listings[tokenIds[i]];
        }
    }
}

The attacker's onERC1155BatchReceived can re-enter buyBatch before the listings are deleted, buying the same tokens multiple times.

Fix: Clear state before the external call. Apply nonReentrant to all marketplace functions.


Unlimited Mint Exploit (Missing Access Control)

// VULNERABLE: public mint with no supply cap or role
contract FreeMint is ERC721 {
    uint256 private _tokenIdCounter;

    function mint(address to) external {
        // No access control, no supply limit, no price
        _mint(to, _tokenIdCounter++);
    }
}

This seems obvious, but it appears in real projects where mint is exposed for testing and never restricted before deployment. A single transaction can drain gas minting thousands of tokens, and secondary market value collapses immediately.

Fix: onlyOwner or a MINTER_ROLE, plus a MAX_SUPPLY cap.


NFT Security Checklist

Minting:
- [ ] Supply cap enforced before mint, not after
- [ ] nonReentrant on all mint functions
- [ ] State updated before _safeMint calls
- [ ] Off-chain signatures use EIP-712 with chainId and contract address
- [ ] Signatures have expiry and per-address nonces

Metadata:
- [ ] BaseURI is immutable or emit EIP-4906 events on change
- [ ] Metadata hosted on IPFS or on-chain (not centralized API)
- [ ] Token existence check in tokenURI (revert on non-existent)

Transfers:
- [ ] Royalty enforcement strategy is documented and intentional
- [ ] Operator allowlist is accurate and up-to-date
- [ ] setApprovalForAll cannot be used with unapproved operators (if enforcing royalties)

ERC-1155 specifics:
- [ ] onERC1155Received and onERC1155BatchReceived are reentrancy-safe
- [ ] Batch operations update state before external calls
- [ ] Token type IDs are validated against expected ranges


Scanner Coverage for NFT Contracts

Vulnerability Slither Mythril Semgrep AI
safeTransferFrom reentrancy ⚠️
Centralized metadata control
Missing access control on mint ⚠️
Signature replay (missing chainId) ⚠️
ERC-1155 batch reentrancy ⚠️
Royalty enforcement gaps

Standard tools catch reentrancy and access control basics. NFT-specific issues — royalty enforcement gaps, metadata centralization, cross-chain signature replay — require understanding the business logic of the contract, which static analysis can't do. AI analysis covers this class of issue by reasoning about the contract's intent vs its implementation.


Scan your NFT contract with ContractScan — upload your ERC-721 or ERC-1155 contract and get full coverage across static analysis, symbolic execution, and AI-powered business logic review.


Related: Token Approval Security: Infinite Allowances — signature and approval risks apply to both ERC-20 and ERC-721.

Related: Reentrancy: From The DAO to Euler Finance — deep dive on reentrancy patterns including callback-based variants.

Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract now
Slither + AI analysis — Unlimited quick scans. No signup required.
Try Free Scan →