Appearance
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
- Register: pay
REGISTRATION_FEE, name must be unoccupied or expired, and the format must be valid - Transfer: the owner transfers the name to a new address
- Resolve: set
resolvedToto point to a target address - Renew: pay the fee to extend the registration period
- 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
_ownedNamesarray 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
NameRegisteredevent to detect whether registration succeeded, and prompt the user on failure - Use swap-and-pop correctly in
_removeFromOwned:soliditynames[i] = names[names.length - 1]; names.pop(); break; - Separate the name format validation logic into a pure function for easy testing
- Use OpenZeppelin's
Stringslibrary to assist with string comparisons
7. Technical Highlights
| Point | Description |
|---|---|
| Hash storage | keccak256(name) as the key, protecting privacy |
| Time gating | block.timestamp comparison for expiration mechanism |
| Ownership pattern | NFT-like ownable resource model |
| swap-and-pop | O(1) array element deletion |
| pull-over-push fees | Registration fee paid directly, no separate withdrawal needed |
| Format validation | On-chain per-byte character checking |