The day an artist turned every NFT into a rug
In early 2021, the artist behind a collection called neitherconfirm did something quietly devastating: overnight, every artwork in the collection was replaced with a photograph of an ordinary rug. The collectors still owned their tokens — the blockchain record of who-owns-what was never touched. What changed was what each token pointed to. The artist did it on purpose, to hammer home a point most buyers had never thought about: the vast majority of NFTs do not store the art on the blockchain at all. You own an unforgeable on-chain receipt that points, through an ordinary web link, to a picture sitting on a server somewhere.
Think of an NFT as a stack of four layers, and notice how few of them are actually guaranteed. Layer 1 is the token itself: a row in an ERC-721 contract that says tokenId 7 is owned by 0xAlice. This lives on-chain and is genuinely immutable. Layer 2 is the `tokenURI` — a string the contract hands back, pointing somewhere. Layer 3 is the metadata: a JSON document with a name, a description, and crucially an `image` field. Layer 4 is the media itself — the PNG, SVG, or MP4. Only Layer 1 is guaranteed to be on-chain. Layers 2 through 4 can live anywhere, and that is where every storage decision — and every risk — hides.
Following the pointer: tokenURI and the JSON
The metadata extension of ERC-721 adds one function: `tokenURI(uint256 tokenId)` returns a string URI. During minting the contract assigns the new tokenId; to find that token's metadata you call this function. The most common implementation simply glues a stored base URI onto the token id.
// ERC-721 metadata extension: tokenURI is just a string POINTER.
function tokenURI(uint256 tokenId) public view returns (string memory) {
require(_ownerOf(tokenId) != address(0), "nonexistent token");
// _baseURI might be "ipfs://bafybeigd.../" -> ".../7"
return string.concat(_baseURI, Strings.toString(tokenId));
}That string usually resolves to a JSON document following the de-facto metadata standard (ERC-721's schema plus OpenSea's widely-adopted extensions). Wallets and a marketplace fetch this JSON, render `image`, and turn `attributes` into the rarity filters you browse.
{
"name": "Glyph #7",
"description": "An on-chain generative artwork.",
"image": "ipfs://bafybeigdyrzt5.../7.png",
"external_url": "https://example.art/7",
"attributes": [
{ "trait_type": "Background", "value": "Cobalt" },
{ "trait_type": "Eyes", "value": "Laser" },
{ "display_type": "number", "trait_type": "Generation", "value": 2 }
]
}ERC-1155 does the same job more efficiently. Its `uri(uint256 id)` returns a template containing the literal substring `{id}`, and the client replaces `{id}` with the token id as a lowercase, zero-padded, 64-character hex string. One template like `https://game.example/api/{id}.json` then serves a whole catalogue of items — no per-token storage at all.
Content addressing: a link that is its own checksum
Those `ipfs://...` links you keep seeing are not locations — they're fingerprints. An ordinary HTTP URL tells you where to look: a server, a path. That server can return whatever it likes today and something else tomorrow. IPFS flips the model: the address is a cryptographic hash of the content. Ask the network for `bafybei…` and any node holding the matching bytes can serve them; you verify the answer by re-hashing it yourself. This is called content addressing.
A CID (Content Identifier) wraps a multihash: a few header bytes naming the hash function and digest length, followed by the digest itself. For SHA-256 the header bytes are `0x12` (the function code) and `0x20` (a 32-byte length), then the 32-byte digest; add a CID version and a codec, base-encode the whole thing, and you get `Qm…` (CIDv0, base58) or `bafy…` (CIDv1, base32).
- Take the file's bytes. Large files are split into ~256 KB blocks linked in a Merkle DAG (the same hash-of-hashes idea as a Merkle tree); the root block's hash becomes the CID.
- Hash the (root) block with SHA-256 to get a 32-byte digest.
- Prepend the multihash header bytes `0x12 0x20`, then add the CID version and codec.
- Base-encode the result → `ipfs://bafybei…`.
- Anyone who downloads the content re-runs steps 1–4 and checks the CID matches. If it doesn't, the bytes are wrong — reject them.
This is why an IPFS link physically cannot lie. Change a single pixel and the file's bytes change, the SHA-256 digest changes, and the CID changes with it. The original CID still points only to the original bytes — there is no way to make `bafybei…X` resolve to anything other than the exact content that produced it. Content-addressed media inherits the same tamper-evidence as the chain itself: this is immutability by hash, not by promise.
The catch nobody mentions: IPFS doesn't mean forever
Content addressing guarantees that if you can find the bytes, they're authentic. It guarantees nothing about whether anyone is still storing them. IPFS nodes keep only the data they choose to and garbage-collect the rest, so your file survives exactly as long as some node pins it — `pin` being the instruction never garbage-collect this. If the last pinning node goes offline and nobody else cached the content, it is simply gone: the CID still 'resolves,' but to nothing.
That gap is what pinning services and persistence layers fill. Pinata and web3.storage / nft.storage keep your CIDs pinned; some also push the data into Filecoin, which adds cryptographic proofs of storage that a provider is still holding your exact bytes under a paid contract. Arweave takes a different route entirely: pay once, and an endowment model funds replication for roughly two hundred years — the 'permaweb,' addressed with `ar://`. The common thread: permanence is a service someone pays for, not a property you get for free by uploading to IPFS.
The sharper danger is the plain HTTPS pointer. If `tokenURI` returns `https://api.coolproject.io/meta/7` — or if the JSON's `image` field does — then a server you don't control decides what your token shows. The team can swap the art, the domain can lapse, the company can fold, and your 'immutable' NFT quietly renders a broken-image icon. That is exactly the neitherconfirm demonstration from the opening, and it is the entire difference between a real guarantee and a marketing slogan.
- On a block explorer, call `tokenURI(tokenId)` (or `uri(id)` for an ERC-1155).
- If it starts with `https://`, the art is centralized and mutable — ask who runs that server and what happens when they stop.
- If it's `ipfs://<cid>`, fetch the JSON and confirm the `image` field is also `ipfs://`, not an `http` URL hiding one layer down.
- Check that something reputable actually pins the content (the project's own node, nft.storage, a Filecoin deal, Arweave). A CID nobody pins is a CID that can vanish.
- Ask whether the contract owner can still change the base URI. A 'reveal' that swaps the base URI is proof the owner can swap it again.
Fully on-chain: art that lives inside the contract
The only way to delete every external dependency is to put the art on-chain. `tokenURI` doesn't have to return a link at all — it can return a data URI, with the JSON, and the image inside it, encoded directly into the returned string. Nothing is fetched from anywhere. The outer shape is `data:application/json;base64,<base64-json>`, and inside that JSON the `image` value is itself `data:image/svg+xml;base64,<base64-svg>`.
// Fully on-chain: the token IS its own metadata + image. No server, no IPFS.
function tokenURI(uint256 id) public pure returns (string memory) {
string memory svg = string.concat(
'<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320">',
'<rect width="320" height="320" fill="#', _bg(id), '"/>',
'<text x="160" y="172" font-size="48" text-anchor="middle" fill="#fff">#',
Strings.toString(id),
'</text>',
'</svg>'
);
string memory json = string.concat(
'{"name":"Glyph #', Strings.toString(id), '",',
'"description":"Fully on-chain. No IPFS, no server.",',
'"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}'
);
return string.concat(
"data:application/json;base64,",
Base64.encode(bytes(json))
);
}The honest catch is cost. On-chain storage is brutally expensive: writing one fresh 32-byte slot costs about 20,000 gas (`SSTORE`), so a few-kilobyte image runs to hundreds of slots and an eye-watering gas bill. That's why on-chain projects either generate their art from a tiny seed at read time, or store it in heavily compressed form. Autoglyphs (Larva Labs, 2019) was the first fully on-chain art: the contract draws each glyph from a short algorithm, storing almost nothing per token. Nouns store each character as run-length-encoded pixels against a shared palette, then assemble the SVG in the contract. Even CryptoPunks were originally only half on-chain — the 2017 contract stored a hash of the composite image as proof of authenticity, while the images themselves were hosted off-chain and only moved fully on-chain in 2021.
Proving it never changed — and a buyer's two-minute checklist
Even with off-chain media, you can nail down immutability cryptographically by committing to a hash on-chain before anyone can game it. Bored Ape Yacht Club published a provenance record: it took the SHA-256 hash of every one of the 10,000 images, concatenated those hashes into one long string, hashed that once more, and stored the result on-chain before the reveal. Paired with a random starting index (derived from a future block), this proves the team could neither reorder the collection nor swap any image after the fact — anyone can re-hash the images today and check the commitment still matches. That is how you turn a mutable-looking pointer into a verifiable promise.
Pull it all together and you can read any collection's true durability straight off its contract. A token standard like ERC-721 only standardizes the pointer; durability is a separate, deliberate choice the team either made or didn't. Ranked roughly from strongest to weakest:
- Fully on-chain (SVG / generative): the strongest — the art cannot disappear or change without rewriting the chain itself.
- IPFS / Arweave with a committed CID and real pinning: strong — content-addressed and tamper-evident, as long as someone keeps the bytes alive.
- IPFS but unpinned, or reachable only via a single gateway URL: fragile — authentic if found, but liable to vanish.
- Centralized HTTPS server: weakest — mutable, and only as durable as one company and one domain registration.