For developers

Code in the contracts

The parts of the implementation that are specific to this protocol rather than boilerplate — emoji segmentation in TVM, domain-separated attestations, the confusable byte scan, and carry-value applied to ownership.

The contracts are written in Tolk 1.4.2 and compile to TVM. What follows is the handful of places where an emoji name service needs to do something a token or a text registry does not.

Counting emoji inside TVM

Pricing tiers, the singles lock and the permanent-upgrade right all key on how many emoji a name is. Segmenting emoji clusters means zero-width joiners, variation selectors, keycaps, tag sequences and regional-indicator pairs — which is why the count is attested off-chain in the first place.

Attesting it is not the same as trusting it. The registrar decodes UTF-8 and counts clusters itself, in the byte scan it was already running, and refuses any attested tier that disagrees:

if (complete) {
    // A codepoint CONTINUES the current unit when it is a joiner, a modifier,
    // or the codepoint a joiner reaches for. Everything else opens a new unit.
    val isZwj = cp == 0x200D;
    val isRi  = cp >= 0x1F1E6 && cp <= 0x1F1FF;
    var newUnit = true;
    if (prevWasZwj) {
        newUnit = false;                       // ZWJ joins what follows
    } else if (isZwj
        || cp == 0xFE0F                        // VS-16
        || cp == 0x20E3                        // combining keycap
        || (cp >= 0xE0020 && cp <= 0xE007F)    // tag characters
        || (cp >= 0x1F3FB && cp <= 0x1F3FF)) { // skin tones
        newUnit = false;
    } else if (isRi && riOpen) {
        newUnit = false;                       // closes a flag pair
    }
    if (newUnit) { units += 1; }
    prevWasZwj = isZwj;
    riOpen = isRi ? !riOpen : false;
}
...
assert(units == emojiCount) throw ERR_EMOJI_COUNT_MISMATCH;

The same algorithm exists in TypeScript as a reference implementation, and the two are proven equivalent over the entire accepted namespace — 3,520 accepted units and 14,080 generated multi-unit names, with zero disagreement — plus a differential test that drives hundreds of names through the on-chain getter and compares.

Why exact equality, not a bound The first version of this fix bracketed the claim by codepoint count. It caught 🚀 attested as two emoji and nothing else — 🇺🇸 ❤️ 5️⃣ 👨‍👩‍👧‍👦 🏳️‍🌈 👩‍💻 are all single emoji spanning two to seven codepoints, and every one of them still walked past the singles lock. Bracketing is the wrong shape for the problem. Segmenting properly is the only thing that closes it, and it costs one extra branch per byte on a loop that was already running.

The confusable byte scan

The blocked look-alikes are enforced in TVM so a compromised signer cannot mint one. They collapse into contiguous codepoint ranges that share their leading UTF-8 bytes, so each costs one prefix match plus a bound on the final byte:

// --- blocked confusable variants, 4-byte sequences (F0 9F xx yy) ---
if (b1 == 0xF0 && b2 == 0x9F) {
    // clock faces U+1F551..1F567  survivor 🕐 U+1F550
    if (b3 == 0x95 && b >= 0x91 && b <= 0xA7) { throw ERR_CONFUSABLE_VARIANT; }
    // moon phases — keep 🌑🌒🌓🌔🌕, block the mirrored waning half
    if (b3 == 0x8C && b >= 0x96 && b <= 0x98) { throw ERR_CONFUSABLE_VARIANT; }
    // 🙁 U+1F641  survivor 😕 U+1F615
    if (b3 == 0x99 && b == 0x81) { throw ERR_CONFUSABLE_VARIANT; }
}
// --- 3-byte sequences: ♥ U+2665 (keep ❤) · ☹ U+2639 (keep 😕) ---
// 0xE2 is a 3-byte lead byte and can never appear as a continuation byte of a
// 4-byte sequence, so this cannot collide with the above.
if (b2 == 0xE2 && ((b3 == 0x99 && b == 0xA5) || (b3 == 0x98 && b == 0xB9))) {
    throw ERR_CONFUSABLE_VARIANT;
}

The survivor of each class differs from its variants in that final byte and is therefore never matched — 😮 U+1F62E lives, 😯 U+1F62F, one codepoint away, does not.

A separate verifier lifts these clauses out of the contract source by balanced-paren extraction and evaluates them directly against the policy table, so there is no second copy of the rule to drift. It exists because an earlier verifier kept its own hardcoded copy of the byte ranges, and that copy went stale.

Domain-separated attestations

The attestation used to be H(nameHash ‖ emojiCount ‖ contentHash ‖ sigValidUntil). It named the name but not the contract and not the ruleset, so one harvested signature was valid on every deployment sharing the signer key, and a signature produced under one normalization policy kept working after that policy had been replaced and locked.

The payload no longer fits in 1023 bits, so the domain rides in a referenced cell:

const ATTESTATION_DOMAIN: int = 0x454E5301; // "ENS\x01"

fun attestationHash(nameHash: uint256, emojiCount: int, contentHash: uint256,
                    sigValidUntil: int, policyHash: uint256): uint256 {
    val domain = beginCell()
        .storeUint(ATTESTATION_DOMAIN, 32)
        .storeAddress(contract.getAddress())
        .storeUint(policyHash, 256)
        .endCell();
    return beginCell()
        .storeUint(nameHash, 256)
        .storeUint(emojiCount, 16)
        .storeUint(contentHash, 256)
        .storeUint(sigValidUntil, 64)
        .storeRef(domain)
        .endCell()
        .hash();
}

This is the subwallet_id pattern that telemint and every TON wallet use, applied to attestations. Changing the published policy now revokes every outstanding signature — which is what a one-way policy lock was always supposed to mean.

Carry-value applied to ownership

TON is asynchronous: by the time a reply reaches the requester, the state it describes may have moved. The standard defence is the carry-value pattern — send the value with the message rather than asking for it — and it applies just as well to ownership as it does to tokens.

Every ownership sync carries a nonce minted with the name. A stale sync arriving late cannot revert a re-registration, because the registrar compares the nonce it is given against the one it issued:

if (prior.isFound) { nonce = (prior.loadValue().syncNonce as int) + 1; }

A lapsed holder replaying an old ResyncOwner cannot claw a name back, and a relay echoing a stale nonce is dropped rather than applied. Renewal deliberately does not bump the nonce, so a transfer that happens mid-renewal still syncs.

Staged signer rotation with proof of possession

Rotating to a key nobody holds disables every issuance path permanently, with no way back — it is the one message where a typo is unrecoverable. So the incoming key must sign for itself, and the rotation serves the same timelock as an admin handover:

UpdateSigner => {
    assert(in.senderAddress == st.admin) throw ERR_NOT_ADMIN;
    assert(msg.newSignerPublicKey != 0) throw ERR_BAD_SIGNER_KEY;
    // proof-of-possession: only the holder of the new key can produce this
    assert(isSignatureValid(signerRotationHash(msg.newSignerPublicKey),
                            msg.proof.load().sig as slice,
                            msg.newSignerPublicKey)) throw ERR_BAD_SIGNATURE;
    applyPendingTimelock(mutate st, nowTime);
    var auxU = st.auxData();
    auxU.pendingSigner = msg.newSignerPublicKey;
    auxU.pendingSignerAt = (nowTime + (st.adminTimelock as int)) as uint32;
    ...
}

ApplySigner is permissionless on purpose — the rotation was already authorised when it was staged, so leaving promotion to the admin alone would let a lost admin key strand a perfectly good signer.

Escrow that cannot be spent

The registrar custodies auction bids, so its balance is not its money. Free balance is computed by subtracting every live bid, and the withdrawal handler asserts against the same expression the getter reports:

fun opsReserve(resolvers: map<uint256, address>): int {
    return MIN_STORAGE + countResolvers(resolvers) * SYNC_VALUE * SYNC_RESERVE_TRANSFERS;
}

The reserve scales with the resolver count because each registered resolver is an outgoing message the registrar must still be able to fund at settlement time. An unbounded resolver set was a real finding: enough resolvers and a lot could neither settle nor cancel, stranding the escrow permanently. The set is now bounded and the reserve is sized to the bound.