ERC-721 vs LSP·8 Ethereum/EVM standard comparison
ERC-721 standardized NFT ownership. LSP8 keeps the ownership model, then upgrades token IDs to bytes32, replaces tokenURI with typed per-token data, and routes every transfer through LSP1.
Spec diff.
| ERC-721 | LSP·8 | |
|---|---|---|
| token ID type | uint256 | bytes32 |
| id semantics | numeric / counter | number, hash, serial, encoded reference |
| metadata pointer | tokenURI(id) → string | getDataForTokenId(id, key) → bytes |
| dynamic updates | MetadataUpdate event (ERC-4906) — signal only | setDataForTokenId — typed write |
| transfer hook | onERC721Received (only on safeTransferFrom) | universalReceiver (LSP1) on every transfer |
| transfer data payload | bytes _data (only on safeTransferFrom) | bytes data on every transfer + force flag |
| off-chain integrity | trust the host | VerifiableURI option in LSP4 |
| per-token data writes | external — server / proxy / extra mappings | setDataForTokenId — typed, on-chain, permissioned |
| interface ABI | ERC-721 selectors — consumed by code that hardcodes them | different selectors — distinct dispatch from ERC-721 |
How they differ structurally
ERC-721 is ownerOf(tokenId) plus a transfer event, with tokenURI(tokenId) returning a
string. Everything else — dynamic metadata, receiver behavior, marketplace cooperation — is
convention layered on top.
LSP8 keeps ownerOf-equivalent behavior but widens the token ID to bytes32 and replaces
tokenURI with a typed per-token key-value store. Want a content hash as the ID? Store the
hash. Want per-token attributes that change at mint, on reveal, on a game event? Set the data
key, gated by whatever permissions you’ve configured on the asset contract.
What changes for downstream code
LSP8 publishes different function selectors. A contract that calls IERC721(token).transferFrom(...)
or reads tokenURI(id) won’t accidentally hit LSP8 — the ABIs are disjoint by design. That’s a
feature: it stops legacy code from misinterpreting an LSP8 asset as an ERC-721 and treating its
data incorrectly. The cost is real: anything you build against LSP8 has to call its functions
explicitly.
bytes32 is the small detail that matters
A uint256 token ID is fine when IDs are sequential counters. The moment IDs need to mean
something (a hash of mint inputs, a serial keyed to off-chain state, a reference encoded with
its provenance), uint256 forces a side mapping. bytes32 makes the ID itself the data
structure.