Alert Source Discuss
📢 Last Call Standards Track: ERC

ERC-7634: Limited Transfer Count NFT

An ERC-721 extension that caps how many times an individual token can be transferred

Authors Qin Wang (@qinwang-git), Saber Yu (@OniReimu), Shiping Chen <shiping.chen@data61.csiro.au>
Created 2024-02-22
Last Call Deadline 2025-12-16
Requires EIP-165, EIP-721

Abstract

This standard extends ERC-721 with a mechanism that lets token owners/minters cap how many times a specific token can be transferred. It specifies functions to set and read a per-token transfer limit, to query a per-token transfer count, and defines transfer-time hooks to enforce the cap. The goal is fine-grained, enforceable transfer restrictions while preserving ERC-721 compatibility.

Motivation

Once NFTs are sold, they detach from their minters and can be perpetually transferred. Yet many situations require tighter control over secondary movement.

First, limiting the number of transfers can help preserve value (e.g., premium auctions, IP that becomes CC0 after a finite number of transfers, or game items that “wear out” and burn at a threshold).

Second, capping transfer frequency can reduce risks from adversarial arbitrage (e.g., HFT-like behavior) by offering an easy-to-deploy throttle.

Third, bounding re-staking cycles of NFT positions (e.g., proof-of-restake) can dampen recursive leverage and mitigate bubble dynamics.

Key Takeaways

Controlled Value Preservation. Scarcity via a transfer cap can help maintain value over time.

Ensuring Intended Usage. Limits keep usage aligned with intent (e.g., limited editions less prone to flip cycles).

Expanding Use Cases. Memberships/licenses with bounded transferability become practical.

Easy Integration. An extension interface (IERC7634) layers on top of ERC-721, easing adoption without breaking compatibility.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

  • setTransferLimit: establishes the transfer limit for a tokenId.
  • transferLimitOf: returns the transfer limit for a tokenId.
  • transferCountOf: returns the current transfer count for a tokenId.

Counting and enforcement scope. Implementations MUST enforce the cap on native ERC-721 transfers of the underlying token (i.e., transfers where from != address(0) and to != address(0)). Incrementing the count SHOULD occur only after a successful native transfer. Mint and burn operations MUST NOT increment the count.

Implementers MUST provide the following interface:


pragma solidity ^0.8.4;

/// @title IERC7634 Interface for Transfer-Capped ERC-721 Tokens
/// @dev ERC-7634 is an extension interface intended to be implemented alongside ERC-721
interface IERC7634 {
    /**
     * @dev Emitted after a successful native transfer when the per-token count increases.
     */
    event TransferCountIncreased(uint256 indexed tokenId, uint256 newCount);

    /**
     * @dev Emitted when the per-token transfer limit is set or updated.
     */
    event TransferLimitUpdated(uint256 indexed tokenId, uint256 previousLimit, uint256 newLimit);

    /**
     * @dev Returns the current transfer count for a tokenId.
     */
    function transferCountOf(uint256 tokenId) external view returns (uint256);

    /**
     * @dev Sets the transfer limit for a tokenId. Callable by owner or approved.
     * @param tokenId The token id to set the limit for.
     * @param limit The maximum number of native transfers allowed.
     */
    function setTransferLimit(uint256 tokenId, uint256 limit) external;

    /**
     * @dev Returns the transfer limit for a tokenId.
     */
    function transferLimitOf(uint256 tokenId) external view returns (uint256);
}
    

Rationale

Tracking and hooks

transferCountOf and transferLimitOf expose state needed to enforce a cap. The count should only increase after a successful native transfer (not on mint/burn). Separating TransferLimitUpdated from TransferCountIncreased makes it clear that the former is an administrative change while the latter is derived from runtime transfers.

Backwards Compatibility

This standard is fully compatible with ERC-721. Existing contracts can adopt it by adding the new interface and hooks without changing ERC-721 semantics.

Extensions

This standard can be enhanced with additional advanced functionalities alongside existing NFT protocols. For example:

  • Incorporating a burn function (e.g., ERC-5679) would enable NFTs to automatically expire after reaching their transfer limits, akin to the ephemeral nature of Snapchat messages that disappear after multiple views.

  • Incorporating a non-transferring function, as defined in the SBT standards, would enable NFTs to settle and bond with a single owner after a predetermined number of transactions. This functionality mirrors the scenario where a bidder ultimately secures a treasury after participating in multiple bidding rounds.

Reference Implementation

Below is a recommended pattern. It enforces the cap pre-transfer, increments the count post-transfer, ignores mint/burn for counting, and emits the clarified events. Implementations commonly override _beforeTokenTransfer to enforce transferCount < transferLimit and _afterTokenTransfer to increment the count and emit TransferCountIncreased.


pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC7634.sol";

/// @title Transfer-Capped ERC-721 (ERC-7634)
/// @dev Example implementation of ERC-7634 alongside ERC-721
contract ERC7634 is ERC721, IERC7634 {
    // tokenId => current transfer count
    mapping(uint256 => uint256) private _transferCounts;
    // tokenId => max allowed native transfers
    mapping(uint256 => uint256) private _transferLimits;

    function transferCountOf(uint256 tokenId) public view override returns (uint256) {
        require(_exists(tokenId), "ERC7634: nonexistent token");
        return _transferCounts[tokenId];
    }

    function setTransferLimit(uint256 tokenId, uint256 limit) public override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7634: not owner/approved");
        uint256 prev = _transferLimits[tokenId];
        _transferLimits[tokenId] = limit;
        emit TransferLimitUpdated(tokenId, prev, limit);
    }

    function transferLimitOf(uint256 tokenId) public view override returns (uint256) {
        require(_exists(tokenId), "ERC7634: nonexistent token");
        return _transferLimits[tokenId];
    }

    /// @dev Enforce transfer limit on native transfers (exclude mint/burn).
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        if (from != address(0) && to != address(0)) {
            require(
                _transferCounts[tokenId] < _transferLimits[tokenId],
                "ERC7634: transfer limit reached"
            );
        }
        super._beforeTokenTransfer(from, to, tokenId);
    }

    /// @dev Increment count only after successful native transfer and emit event.
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 tokenId,
        uint256 quantity
    ) internal virtual override {
        if (from != address(0) && to != address(0)) {
            unchecked { _transferCounts[tokenId] += 1; }
            emit TransferCountIncreased(tokenId, _transferCounts[tokenId]);

            if (_transferCounts[tokenId] == _transferLimits[tokenId]) {
                // Optional: perform action exactly when the cap is reached (e.g., _burn(tokenId))
            }
        }
        super._afterTokenTransfer(from, to, tokenId, quantity);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721)
        returns (bool)
    {
        return interfaceId == type(IERC7634).interfaceId || super.supportsInterface(interfaceId);
    }
}

Security Considerations

  • Scope with wrappers. The cap applies only to native ERC-721 transfers of the underlying token. Any owner can cheaply wrap a token and transfer a separate wrapper token; such downstream transfers cannot be prevented without breaking ERC-721 composability. As a result, this standard does not provide an unbypassable guarantee that, for example, a game item will always wear out or that secondary trading is globally capped; it standardizes a primitive for budgeting native transfers that ecosystems may choose to coordinate around. Deployments that want stronger effective guarantees MAY add optional mitigations such as recipient allowlists/registries or “compliant wrapper” patterns that mirror counts/limits, trading off openness for enforcement.

  • Limit mutability. Consider making limits immutable once set (or only decreasing), to prevent tampering.

  • Gas. Avoid heavy logic in hooks; extensions (e.g., burn on cap) should remain gas-safe.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Qin Wang (@qinwang-git), Saber Yu (@OniReimu), Shiping Chen <shiping.chen@data61.csiro.au>, "ERC-7634: Limited Transfer Count NFT [DRAFT]," Ethereum Improvement Proposals, no. 7634, February 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7634.