ENS v2 introduces a unique challenge: token IDs need to change in certain scenarios to maintain security and data integrity. This creates a system where:
Token IDs are mutable - They can change over time for the same name
Canonical IDs are stable - Internal representation remains constant
Data persistence - Registry data survives token regeneration
Critical Security Feature: Token ID regeneration prevents marketplace griefing and ensures clean ownership after expiry.
// External-facing ID (ERC1155 token ID)// Changes when the token is regenerateduint256 tokenId = 0x1234567890abcdef1234567890abcdef1234567890abcdef12345678AABBCCDD;// ^^^^^^^^// Lower 32 bits = version
Entry storage entry = _entry(labelId);uint64 expiry = entry.expiry;if (block.timestamp >= expiry) { // Name is expired and available for re-registration}
2
New Owner Registers
Someone re-registers the expired name:
if (prevOwner != address(0)) { // Burn old token _burn(prevOwner, tokenId, 1); // Increment version counters ++entry.eacVersionId; // Access control version ++entry.tokenVersionId; // Token version // Generate new token ID tokenId = _constructTokenId(tokenId, entry);}
3
Why This Matters
Security: The new owner gets a fresh token ID with no residual roles from the previous owner:
// Old token ID: 0x...00000001 (version 1)// All old roles were assigned to version 1// New token ID: 0x...00000002 (version 2)// No roles assigned yet - clean slate!
function _constructResource(uint256 anyId, Entry storage entry) internal view returns (uint256) { return LibLabel.withVersion( anyId, _isExpired(entry.expiry) ? entry.eacVersionId + 1 // Next version for expired names : entry.eacVersionId // Current version for active names );}
Special Behavior: Expired names use eacVersionId + 1 so they have a clean permission space when re-registered.Reference: PermissionedRegistry.sol:481-495
Most registry functions accept anyId which can be:
A canonical ID (label hash)
A token ID (with version bits)
A resource ID (with EAC version bits)
// All of these work:registry.setResolver(labelHash, resolver); // Canonical IDregistry.setResolver(currentTokenId, resolver); // Current token ID registry.setResolver(oldTokenId, resolver); // Old token ID (if not expired)// The function internally derives the current token IDfunction setResolver(uint256 anyId, address resolver) public virtual { (uint256 tokenId, Entry storage entry) = _checkExpiryAndTokenRoles( anyId, RegistryRolesLib.ROLE_SET_RESOLVER ); // tokenId is now the CURRENT token ID regardless of input}
// 1. Current owner (respects version and expiry)address owner = registry.ownerOf(anyId);// Returns address(0) if:// - Token ID is not current// - Name is expired// 2. Latest owner (ignores version and expiry)address latestOwner = registry.latestOwnerOf(tokenId);// Returns the owner of this specific token ID// Even if it's an old version or expired
Use Cases:
ownerOf(): Standard ownership checks, marketplace integration
// ✅ GOOD: Store canonical IDstruct MyAppData { uint256 nameId; // Store: labelHash or canonical ID // ...}// Always derive current token ID when neededfunction getCurrentToken(uint256 nameId) public view returns (uint256) { return registry.getTokenId(nameId);}
// ❌ BAD: Store token IDstruct MyAppData { uint256 tokenId; // This can become stale! // ...}// Token ID might be outdated
function checkPermission(uint256 anyId, address user) public view returns (bool) { // Get current resource ID (handles version automatically) uint256 resource = registry.getResource(anyId); // Check permissions against current version return registry.hasRoles(resource, REQUIRED_ROLE, user);}
Use latestOwnerOf() if you need the owner of a specific version.
Token Regeneration During Transfers
Token IDs don’t regenerate during transfers:
function _update(address from, address to, uint256[] memory tokenIds, ...) { // Transfers DO NOT regenerate tokens // Only role changes regenerate}
However, roles ARE transferred to the new owner, which DOES regenerate:
if (externalTransfer) { for (uint256 i; i < tokenIds.length; ++i) { _transferRoles(getResource(tokenIds[i]), from, to, false); // This triggers _onRolesGranted → _regenerateToken }}
Canonical ID Collisions Are Impossible
Since canonical IDs are derived from label hashes: