Skip to content
On this page

L1-13: Name System

1. Problem

Build a decentralized on-chain name registry system -- mapping human-readable names to Ethereum addresses, similar to a simplified version of ENS (Ethereum Name Service). Core features: name registration (paid), ownership transfer, resolver address configuration, name renewal, and expiration-based release.

2. Why

ENS is one of the most important pieces of Web3 infrastructure -- mapping names like vitalik.eth to 0x... addresses. Understanding the core patterns of a name registry system is fundamental to building any on-chain identity system:

  • Name mapping: human-readable name -> machine address
  • Ownership model: registrants have full control over their names
  • Time-locking: registrations have a duration; upon expiry, the name is released for others
  • Fee mechanism: prevents name squatting and hoarding

This challenge simplifies ENS: no multi-level domains (the .eth suffix is managed by the registry), no resolver contract separation -- it focuses on the core logic of name ownership management.

3. Solution

Contract Architecture

solidity
contract NameSystem {
    struct NameRecord {
        address owner;       // name owner
        address resolvedTo;  // resolved target address
        uint256 expiresAt;   // expiration timestamp
    }

    uint256 public constant REGISTRATION_FEE = 0.01 ether;
    uint256 public constant REGISTRATION_PERIOD = 365 days;
    uint256 public constant MIN_NAME_LENGTH = 3;
    uint256 public constant MAX_NAME_LENGTH = 32;

    mapping(bytes32 => NameRecord) private _records;
    mapping(address => bytes32[]) private _ownedNames;
}

Core Flow

  1. Register: pay REGISTRATION_FEE, name must be unoccupied or expired, and the format must be valid
  2. Transfer: the owner transfers the name to a new address
  3. Resolve: set resolvedTo to point to a target address
  4. Renew: pay the fee to extend the registration period
  5. Expired re-registration: anyone can re-register an expired name

Key Implementation Details

  • Names are stored as bytes32 (keccak256 hash), protecting privacy and ensuring fixed length
  • The _ownedNames array tracks the list of names owned by each address
  • Name format validation: only a-z, A-Z, 0-9, and - are allowed, length 3-32
  • Use swap-and-pop to remove names from the previous owner's array during transfers

4. Pitfalls Encountered

4.1 Race Condition on Expired Names

Two users may attempt to register the same recently expired name within the same block. Since Solidity transactions are atomic, only one will succeed, and the other will have its gas wasted.

4.2 Unbounded Growth of _ownedNames Array

If a user frequently registers and transfers names, the swap-and-pop deletion in _ownedNames must be performed correctly. If the new element after popping happens to be a duplicate (due to incorrect swap logic), name tracking may be lost.

4.3 Performance of Name Format Validation

The per-byte check in _isValidName is O(32) for 32-byte names -- gas-acceptable. However, if the length cap is extended, the gas cost of character checking grows linearly.

4.4 False Sense of Security from Hash Collisions

Using keccak256(name) as the key means two different names cannot collide. But it also means the name cannot be reverse-derived from the hash -- the frontend must maintain a mapping from name to hash.

5. Why the Pitfalls Happen

5.1

Ethereum transactions execute sequentially (within a single block); there is no traditional parallel race condition. However, two pending transactions may be bundled by the same builder -- only one of user A's registration transaction and user B's registration transaction will succeed. B pays gas for a failed transaction.

5.2

Swap-and-pop is an O(1) array deletion technique, but the implementation must ensure that the popped element is indeed moved to the deleted position and the array length is correctly decremented by one. Common bugs: deleting the wrong index, failing to break out of the loop, or incorrect array length after pop.

5.3

Solidity has no native regex support. Per-byte comparison is the only approach. For large datasets, consider validating the format off-chain and submitting via a signature.

6. How to Resolve the Pitfalls

  • The frontend can listen for the NameRegistered event to detect whether registration succeeded, and prompt the user on failure
  • Use swap-and-pop correctly in _removeFromOwned:
    solidity
    names[i] = names[names.length - 1];
    names.pop();
    break;
    
  • Separate the name format validation logic into a pure function for easy testing
  • Use OpenZeppelin's Strings library to assist with string comparisons

7. Technical Highlights

PointDescription
Hash storagekeccak256(name) as the key, protecting privacy
Time gatingblock.timestamp comparison for expiration mechanism
Ownership patternNFT-like ownable resource model
swap-and-popO(1) array element deletion
pull-over-push feesRegistration fee paid directly, no separate withdrawal needed
Format validationOn-chain per-byte character checking

Built with AiAda