Sitemap
DEXignation

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

The Bug That Doesn’t Throw: Making NFT Name Transfers Safe on DEXignation

--

There’s a category of smart-contract bug that never reverts, never logs an
error, and never trips a test you didn’t think to write. It just quietly sends
someone’s money to the wrong person. We found one in our own naming service
before launch — and the story of fixing it is really a story about what it means
for a name to *belong* to someone on-chain.

## What a name is supposed to do

DEXignation issues `.dex` names as ERC-721 NFTs on Polygon. A name like
`roy.dex` isn’t decorative — it’s a routing target. You send funds to `roy.dex`,
an agent pays `roy.dex`, a profile resolves from `roy.dex`. The entire value of
a name is that it points to the address its **current owner** controls.

So a name has two halves that must always agree: the **NFT** (who owns it) and
the **resolution** (where it points). The moment those two drift apart, the name
lies.

## The drift

Here’s the scenario that should keep any naming-service author up at night. Alice
owns `roy.dex`. She sells the NFT to Bob — a normal ERC-721 transfer, the kind
every marketplace does. Bob now holds the token. Someone sends funds to
`roy.dex`.

In our v1 contracts, those funds went to **Alice**.

The NFT moved. The resolution didn’t. `roy.dex` kept pointing at Alice’s address
because nothing in the system connected “the token changed hands” to “the name’s
records should change too.” No revert. No event. No failing test. Just value
flowing to someone who no longer owned the name.

## Why it drifted

When we traced it through the code, the gap wasn’t one broken line — it was a
missing connection between three contracts that each worked fine on their own.

The registrar (the ERC-721) had no transfer hook, so a transfer changed
`ownerOf` and nothing else. The registry kept its own ownership record — the one
that actually gates who can edit a name’s resolution — and it only updated when
someone manually called a reclaim function. A plain NFT sale never did. And the
resolver, which stored the actual records, had no concept of “these belonged to
a previous owner,” so every stale record survived the transfer fully intact.

Three contracts, three reasonable designs, and a fund-loss bug living in the
space between them. That’s the uncomfortable lesson: the dangerous bugs often
aren’t *in* a component, they’re in the assumptions about how components relate.

## The fix: clear the records, but keep the history

Two decisions mattered.

Get Roy’s stories in your inbox

Join Medium for free to get updates from this writer.

First, *when* to clear a name’s records. The answer is the obvious one — on every
genuine transfer of ownership. The interesting part was hooking into the ERC-721
transfer lifecycle itself, so that the act of moving the token *is* the act of
moving control. No separate call to remember, no way to forget.

Second, *how* to clear them. The naive approach is to delete each record. But
Solidity mappings can’t be enumerated — to delete every record you’d have to
already know every key ever written, across addresses, text entries,
contenthashes, profiles, ABIs, and agent records. That’s expensive and, worse,
it’s the kind of thing that silently misses a key one day.

Instead we version the records. Every record lives under a version number, and a
transfer just increments it. One cheap operation makes every record kind from
the old owner unreachable at once — nothing to enumerate, nothing to miss.

And there’s a bonus that fit our priorities exactly: the old records don’t get
destroyed, they get *superseded*. They remain on chain under the previous
version. So the name still has a complete, auditable history of who controlled
it and where it pointed at each stage — which, for a service where names route
money, is something we actively wanted rather than something to discard. (If
this sounds familiar, it’s the same versioning approach ENS’s resolver has used
in production for years.)

## The subtlety: registration is also a transfer

The cleanest bugs to fix are the ones whose fix introduces a new bug, and we hit
ours immediately. After wiring up the transfer hook, a test that had passed for
months started failing: freshly registered names had no address record.

The cause was a nice piece of irony. Our registration flow delivers the NFT by
having the controller mint the token to *itself*, set the initial address
record, then transfer the token to the buyer. That final transfer is — correctly
— detected by our new hook, which dutifully invalidated the address we’d just
set. The safety mechanism was working perfectly; it just couldn’t tell “delivery
to a new owner” apart from “resale to a new owner.”

The fix was to teach it the difference. Transfers originating from a registration
controller are deliveries, not resales, so they skip invalidation. Every other
user-to-user transfer invalidates. One condition, and the two cases that look
identical to ERC-721 become distinct to us.

## Proving it

A fix to a never-throws bug is only as trustworthy as its tests, so we leaned
hard on them. The suite grew to 155 passing tests, including a battery that
registers a name, populates all six record kinds, transfers it, and asserts that
control moved to the new owner, every record reads empty, the old owner can no
longer write, the new owner can, resolution resumes once they do, and the version
counter advanced while the old records persisted on chain. A separate edge-case
set checks the corners: `safeTransferFrom`, approved-operator transfers,
sequential transfers, and that nobody but the registrar can trigger an
invalidation.

Then we left the lab. We deployed to Polygon’s Amoy testnet and ran the whole
thing on a live network — register, set records, transfer to a second wallet,
and read the chain back. Version incremented, control moved, records cleared.
The behavior held outside the deterministic comfort of a local test runner.

Only then did we redeploy to mainnet, verify the source on PolygonScan, and
register the first name on the new contracts.

## What we took away

The bugs that scare me most are the polite ones — the ones that don’t crash,
don’t alert, and look like correct behavior right up until someone loses money.
Defending against them isn’t really about clever code. It’s about noticing the
assumptions sitting in the gaps between your components, and writing the test
that turns a silent failure into a loud one.

A name should never outlive its owner. Now, on DEXignation, it doesn’t.

--

--

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.