Skip to main content
Sigvex

ERC-721 Standard Violations

Detects ERC-721 contracts missing required functions, safe-transfer callbacks, ERC-165 support, or zero-address checks.

ERC-721 Standard Violations

Overview

Remediation Guide: ERC-721 Standard Violations Remediation

The ERC-721 violations detector first classifies a contract as an NFT by checking for the core selectors (balanceOf, ownerOf, transferFrom), then verifies it against the standard. It reports five finding types:

  • Missing required function — a core ERC-721 selector is absent from the dispatch table.
  • Missing ERC-165 support — no supportsInterface(bytes4) implementation, breaking interface discovery for marketplaces and wallets.
  • Missing onERC721Received checksafeTransferFrom does not invoke the receiver callback, so tokens sent to contracts that cannot handle them are lost.
  • Missing zero-address check — transfer or mint paths accept address(0), effectively burning tokens by accident.
  • Potential reentrancy in safe transfer — state is updated after the receiver callback, letting a malicious receiver re-enter mid-transfer.

Why This Is an Issue

Marketplaces, wallets, and bridges assume standard behavior. A missing receiver callback means every safeTransferFrom to a non-receiver contract permanently locks the NFT — the exact failure safeTransferFrom exists to prevent. Missing ERC-165 declarations make platforms refuse to list the collection or, worse, mis-handle it. And because the onERC721Received callback hands control to the recipient, a transfer implementation that updates ownership after the callback is a reentrancy vector: the receiver can call back into the collection while ownership state is stale.

How to Resolve

// Before: "safe" transfer that never checks the receiver
function safeTransferFrom(address from, address to, uint256 id) external {
    _transfer(from, to, id);
}

// After: callback verified, state updated before the external call
function safeTransferFrom(address from, address to, uint256 id) external {
    _transfer(from, to, id); // effects first
    if (to.code.length > 0) {
        require(
            IERC721Receiver(to).onERC721Received(msg.sender, from, id, "")
                == IERC721Receiver.onERC721Received.selector,
            "unsafe recipient"
        );
    }
}

Prefer inheriting an audited implementation (OpenZeppelin ERC721) over reimplementing the standard.

Examples

Vulnerable Code

contract BareNFT {
    mapping(uint256 => address) public ownerOf;

    function transferFrom(address from, address to, uint256 id) external {
        require(ownerOf[id] == from, "not owner");
        // Missing: zero-address check — 'to' may be address(0)
        ownerOf[id] = to;
        // Missing: Transfer event, approval clearing, ERC-165, safe variant
    }
}

Fixed Code

contract StandardNFT is ERC721 {
    constructor() ERC721("Standard", "STD") {}
    // Inherits compliant transfer, safe transfer with callback,
    // zero-address checks, events, and ERC-165 declarations.
}

Sample Sigvex Output

{
  "detector_id": "erc721-violations",
  "severity": "high",
  "confidence": 0.7,
  "description": "Missing onERC721Received check in safeTransferFrom(address,address,uint256): the receiver callback is never invoked, so transfers to contracts can permanently lock tokens.",
  "location": { "function": "safeTransferFrom(address,address,uint256)", "offset": 0 }
}

Detection Methodology

The detector matches the contract’s dispatch table against the canonical ERC-721 and ERC-165 selector sets, then inspects each safe-transfer function body for an external call carrying the onERC721Received selector, for zero-address comparison branches on transfer paths, and for the ordering of storage writes relative to the receiver callback. Findings carry individual confidences (roughly 0.70–0.80) reflecting known false-positive sources — for example, a callback check implemented in a parent contract or library that the flattened bytecode obscures.

Limitations

  • Soulbound or deliberately restricted tokens that omit transfer functions by design are flagged for missing functions; confidence is reduced but not zero.
  • ERC-165 support declared in a proxy’s implementation contract is not visible when scanning the proxy alone.
  • Event emission correctness (parameter indexing) is not fully verified from bytecode.

References