Soludound NFT as separated standart

I’ve been thinking about current Soulbound token implementations and their reliance on ERC-721 as a base. Most existing solutions (like ERC-5192) essentially bolt locking mechanisms onto ERC-721, reverting transfers with custom errors. While this works, it feels architecturally wrong.

The core issue: Soulbound tokens aren’t non-fungible - they’re non-transferable. ERC-721 is built around the concept of transferability. The entire interface revolves around transferFrom, safeTransferFrom, approve, setApprovalForAll - functions that fundamentally contradict what Soulbound means.

When we inherit from ERC-721 and override these functions to revert, we’re carrying dead weight:

  • Storage slots for approvals and operators that will never be used
  • Larger deployment bytecode
  • Higher gas costs on mint/burn due to unnecessary state management
  • Increased Ethereum state growth from unused mappings

Here’s a minimal implementation that covers everything Soulbound needs:

interface SoulboundNFT {
    event Mint(address to, uint256 id);
    event Burn(address from, uint256 id);

    function mint() external returns(uint256, bool);
    function burn(uint256) external;
    function tokenURI(uint256 _tokenId) external view returns (string memory);
}

contract SoulboundToken is SoulboundNFT {
    uint256 private tokenId;
    string private _name;
    string private _symbol;
    string private _baseURI;
    
    mapping(uint256 => address) ownerOf;
    

    constructor(string memory name, string memory symbol, string memory baseURI) {
        _name = name;
        _symbol = symbol;
        _baseURI = baseURI;
    }

    function mint() external virtual returns (uint256 id, bool) {
        tokenId += 1;
        id = tokenId;
        ownerOf[id] = msg.sender;
        emit Mint(msg.sender, id);
        return (id, true);
    }

    function burn(uint256 id) external virtual {
        if (ownerOf[id] != msg.sender) revert ErrNotAnOwner();
        ownerOf[id] = address(0);
        emit Burn(msg.sender, id);
    }

    function tokenURI(uint256 _tokenId) external view returns (string memory) {
        return _baseURI; 
        // or string(abi.encodePacked(_baseURI, Strings.toString(_tokenId)))
    }
}

This drastically reduces codesize, which opens up space for storing additional data directly in the contract - like on-chain SVG metadata.

The semantic argument matters too. When you see ERC-721, you expect transferability. Soulbound tokens represent credentials, achievements, identity - things that by definition shouldn’t move between addresses. Using a transferable token standard and disabling transfers creates conceptual confusion.

Current ERC-721-based SBT implementations aren’t truly “bound to soul” - they’re transferable tokens with transfer restrictions. That’s a workaround, not a solution.

A dedicated standard would:

  • Remove unused storage (approvals, operators)
  • Reduce deployment bytecode significantly
  • Lower gas costs for mint/revoke operations
  • Decrease state growth compared to ERC-721-based implementations
  • Provide clear semantics: mint once, own forever, or burn

The counterargument is ecosystem compatibility - wallets and marketplaces already understand ERC-721. But Soulbound tokens don’t need marketplace support. They’re not meant to be traded. Wallet support just needs ownerOf and metadata URI, which any minimal standard can provide.

Curious what others think - is the convenience of ERC-721 inheritance worth the architectural and efficiency tradeoffs?