Sitemap
DEXignation

DEXignation explores decentralized identity, human-readable wallet naming, and the future of Web3-native digital presence.

An ERC-721 with Fully On-Chain SVG

--

*Part 3 of the DEXignation Series. Estimated read time: 7 min.*

— -

Most NFTs are not on-chain. Their `tokenURI` returns an IPFS or HTTPS
link, and a JSON document at that location points at *another* link
where the image actually lives. Two layers of indirection. Two
opportunities for the artwork to vanish.

DEXignation does it differently. Every `.dex` name’s image, metadata,
and JSON are generated *inside the contract* on every call to
`tokenURI`. As long as the contract exists and the chain runs, the
artwork exists.

This post walks through how that works in roughly 80 lines of Solidity,
and what trade-offs we accepted along the way.

— -

## What “fully on-chain” actually means

There are three things a `tokenURI` returns:

1. A **URI**.
2. That URI resolves to a **JSON document**.
3. The JSON’s `image` field points at an **image**.

“Fully on-chain” means all three live in contract storage or are
computed from it — no network round-trips. The standard technique is
**`data:` URIs**.

A `data:` URI lets you inline content directly into a URL:

```
data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCI+…
data:application/json;base64,eyJuYW1lIjoiYWxpY2UuZGV4Iiwi…
```

Both fields above contain Base64-encoded payloads. The browser, the
NFT marketplace, or the wallet can decode and display them directly.
No server needed.

— -

## The DEXignation implementation

Here’s the full `tokenURI` from `contracts/registrar/DXRegistrar.sol`:

```solidity
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
string memory label = names[tokenId];
if (bytes(label).length == 0) {
label = “?”;
}
string memory dotTld = string.concat(“.”, baseNodeName);
string memory svg = _generateSVG(label, dotTld);
string memory json = string.concat(
‘{“name”:”’, label, dotTld, ‘“,’
‘“description”:”DEXignation Name: ‘, label, dotTld, ‘“,’
‘“image”:”data:image/svg+xml;base64,’,
Base64.encode(bytes(svg)),
‘“}’
);
return string.concat(
“data:application/json;base64,”,
Base64.encode(bytes(json))
);
}
```

Reading it top-to-bottom:

1. `_requireOwned(tokenId)` — OpenZeppelin’s check that the token exists
and has an owner. If not, revert.
2. Look up the original label string from the `names[tokenId]` mapping.
Fall back to `”?”` for safety (shouldn’t happen for properly
registered names).
3. Compose `”.dex”` from `baseNodeName` so the same code works for
future TLDs.
4. Generate the SVG inline.
5. Build the metadata JSON, embedding the SVG as a `data:image/svg+xml`
URI.
6. Wrap the whole JSON in `data:application/json` and return.

That’s the entire metadata pipeline.

— -

## The SVG generator

```solidity
function _generateSVG(string memory label, string memory dotTld)
internal pure returns (string memory)
{
return string.concat(
‘<svg width=”400" height=”400" xmlns=”http://www.w3.org/2000/svg">'
‘<defs><linearGradient id=”bg” x1=”0" y1=”0" x2=”1" y2=”1">’
‘<stop offset=”0%” stop-color=”#07080A”/>’
‘<stop offset=”100%” stop-color=”#0D1117"/>’
‘</linearGradient></defs>’
‘<rect width=”400" height=”400" rx=”20" fill=”url(#bg)”/>’
‘<rect x=”8" y=”8" width=”384" height=”384" rx=”16" fill=”none” stroke=”#00DC82" stroke-opacity=”0.2"/>’
‘<text x=”200" y=”170" text-anchor=”middle” font-family=”sans-serif” font-weight=”bold” font-size=”40" fill=”#00DC82">’,
label,
‘</text>’
‘<text x=”200" y=”220" text-anchor=”middle” font-family=”sans-serif” font-size=”28" fill=”#64748B”>’,
dotTld,
‘</text>’
‘<text x=”200" y=”360" text-anchor=”middle” font-family=”monospace” font-size=”11" fill=”#2D3A48">DEXignation Name Service</text>’
‘</svg>’
);
}
```

The function is `pure` — no state reads, no external calls. For a given
label and TLD, it always produces the same SVG. That makes it a clean
target for off-chain caching by marketplaces.

A few choices worth noting:

- **Sans-serif system font.** Embedding a font in the SVG would inflate
output by 50–100 KB. Falling back to whatever the renderer has is
fine for a name display.
- **Solid colours and a gradient, no images.** Pure SVG primitives so
there’s nothing to load externally.
- **Title and subtitle separation.** The label and the `.dex` are on
different lines so long Korean or unicode labels still read cleanly.

Get Roy’s stories in your inbox

Join Medium for free to get updates from this writer.

— -

## The Base64 double-encoding dance

The outermost wrapper is `data:application/json;base64,<JSON-base64>`.
Inside the JSON, the `image` field is another `data:image/svg+xml;base64,<SVG-base64>`.
We Base64-encode twice.

Why? Because both URIs are syntactically URLs, and a URL can contain
characters that have special meaning in URL parsing (`#`, `?`, `&`,
spaces, quotes). Base64 normalises everything to a URL-safe alphabet.

The decoder does this in reverse:

```
1. Strip the “data:application/json;base64,” prefix.
2. Base64-decode → JSON string.
3. Parse JSON, read `image` field.
4. Strip the “data:image/svg+xml;base64,” prefix.
5. Base64-decode → SVG.
6. Render.
```

We use OpenZeppelin’s `Base64` utility, which is well-tested and gas-
efficient. The full overhead is about 33% more bytes per encoding pass,
i.e. our 1 KB SVG becomes ~1.4 KB Base64'd, embedded in a ~1.5 KB JSON
which becomes ~2 KB Base64'd. Still cheap.

— -

## Why we store the original label

ENS doesn’t store the human-readable label. Only its
`keccak256(label)` (the “labelhash”). This saves a storage slot per
registration. The downside is you can never reconstruct `”alice”` from
`labelhash` — you can only verify a candidate.

DEXignation stores the label:

```solidity
mapping(uint256 => string) names;

// inside register():
names[id] = label;
```

The tradeoff is one extra SSTORE per registration. That’s roughly
20,000 gas. At Polygon’s typical gas price (around 30 gwei) and POL
price (around $0.4), this costs about **$0.0024 per registration**. We
accept that.

In exchange, we get:

- **Renderable `tokenURI`.** The whole point of this post.
- **Future-proofed on-chain features.** Subdomain listings, search,
display logic — all of them benefit from having the canonical label
stored.

— -

## Trade-offs to consider

I’d be lying if I said fully-on-chain is always the right call. Things
you give up:

- **Bigger contract bytecode.** SVG and Base64 logic add bytes. Stay
well under the 24 KB contract limit; we use ~12 KB for the whole
registrar.
- **No high-fidelity art.** A 4 KB SVG of geometric shapes is fine.
A 200 KB photographic NFT is not. Pick the medium accordingly.
- **`view`-call gas cost.** `tokenURI` does work on every call.
Marketplaces cache it, but pay attention if you’re calling it
in a loop.
- **No off-chain enhancement.** With an off-chain renderer you can
update the art design without redeploying. With on-chain SVG, the
art is locked.

For a name service the trade-offs are obviously favourable. For an art
project where the art is the value, you’d want a different answer.

— -

## What you can do next

- **Read the function in context** —
[`DXRegistrar.sol`](https://github.com/DEXignation/dexignation-contracts/blob/main/contracts/registrar/DXRegistrar.sol).
- **Try generating a `tokenURI` locally** — deploy to Hardhat, register
a name, call `tokenURI(tokenId)`, paste the resulting `data:` URI
into your browser. The SVG should render.
- **Customise the SVG** — `_generateSVG` is internal but very easy to
fork. Match it to your brand if you reuse DEXignation as a base.

— -

*Previous: [Part 2 — namehash Explained](./02-namehash-explained.md)*
*Next: [Part 4 — Stopping Front-Running with Commit-Reveal](./04-commit-reveal.md)*

— -

🇰🇷 *Korean version available at [docs.dexignation.com/blog](https://docs.dexignation.com/blog).*

--

--

Roy
Roy

Written by Roy

Full-stack fintech engineer building KookminWallet and DEXignation — focused on self-custody UX, Web3 identity, and decentralized payment systems.

DEXignation
DEXignation

Published in DEXignation

DEXignation explores decentralized identity, human-readable wallet naming, and the future of Web3-native digital presence.