Stagnant ERC extending ERC-721 with 32 bytes of mutable on-chain storage per tokenId, plus read/write accessors and a change event. Pioneered the dynamic-NFT pattern later popularized by ERC-4906 metadata-update events; never reached Final. Status: Stagnant.
- 01dynamic NFT state (game stats, attestations)
- 02small per-token mutable payload without full custom storage
- 03legacy dynamic-NFT integrations
Implement ERC-1948 by extending ERC-721 with `mapping(uint256 => bytes32) private _tokenData;` plus `readData(uint256 tokenId) returns (bytes32)` and `writeData(uint256 tokenId, bytes32 newData)`. `writeData` MUST require `msg.sender == ownerOf(tokenId)` (or be approved operator) and emit `DataUpdated(uint256 indexed tokenId, bytes32 oldData, bytes32 newData)`. Because the spec is Stagnant, prefer composing ERC-4906 (`MetadataUpdate(uint256 tokenId)` event) for marketplace cache busting on top of any per-token data scheme — most modern dynamic NFTs use ERC-4906 + custom storage rather than ERC-1948.
- ⚑Status is Stagnant — no canonical OpenZeppelin implementation. New projects should use ERC-4906 (metadata update event) layered onto custom storage rather than 1948.
- ⚑32 bytes is a tight payload — projects routinely outgrow it (cosmetic flags, level + XP + faction) and are forced to migrate to a struct-per-token layout, breaking the standard interface.
- ⚑Marketplaces and indexers do NOT auto-refresh metadata on `DataUpdated` — without ALSO emitting ERC-4906 `MetadataUpdate`, OpenSea/Blur show stale traits indefinitely.
- ⚑`writeData` is fully owner-controlled — a buyer of a dynamic NFT can erase or alter on-chain history. If history matters (achievements, soulbound stats), restrict writes to the original minter or a game contract, not the holder.
- ⚑Composing with ERC-4626 vaults / ERC-6551 token-bound accounts: bytes32 alone cannot represent a complex sub-account state — use ERC-6551 if you need a full account per token rather than 32 bytes.