The behaviour of names and the name wrapper is best thought of in terms of a state machine, whose states names proceed through over their lifecycles.
State transitions are facilitated by functions on the name wrapper and registrar controller contracts. One function can potentially move a name through multiple states - for example, calling registerAndWrapETH2LD() will register a .eth second-level name, wrap it, and emancipate it, moving it from [Unregistered] to Emancipated state in a single call.
Some state transitions are irrevocable; once a name is Emancipated or Locked, it can only return to being Wrapped or Unwrapped after it expires.
Name States
Unregistered
Names start out in the unregistered state before they are registered, and return to it after they expire.
How to check:
To check if a name is Unregistered, verify that NameWrapper.ownerOf returns address(0) and Registry.owner returns either address(0) or the address of the NameWrapper contract.
Unwrapped
A name that is registered but not managed by the name wrapper is Unwrapped.
Unwrapped names do not expire, with the exception of .eth second-level names, which have behaviour enforced by the .eth registrar.
How to check:
To check if a name is Unwrapped, verify that NameWrapper.ownerOf returns address(0) and Registry.owner returns any address except for address(0) or the address of the NameWrapper contract.
Wrapped
Wrapping an Unwrapped name makes it Wrapped. Wrapped names are managed by the name wrapper, and have ERC1155 tokens, but can be unwrapped at any time, and have no special protections over and above an unwrapped name - for example, the owner of the parent name can still make changes to the name or take back ownership.
Wrapped names do not expire, with the exception of .eth second-level names, which have behaviour enforced by the .eth registrar, and have a wrapper expiry equal to the end of the name’s grace period.
How to check:
To check if a name is Wrapped, verify that:
NameWrapper.ownerOf(node) does not return address(0)
Registry.owner is the NameWrapper contract
- If it’s a .eth name,
registrar.ownerOf(labelhash) must be the NameWrapper contract
If any of these are false, the name should be considered unwrapped.
Emancipated
An Emancipated name provides the assurance that the owner of the parent name cannot affect it in any way until it expires.
A name is Emancipated when the owner of the parent domain gives up control over it. They do this by setting an expiration date - which can be extended at any time - and burning the PARENT_CANNOT_CONTROL fuse over the name. To do this, the parent name must already be in the Locked state.
.eth second-level names are automatically Emancipated when wrapped, and their expiry is fixed at the end of the grace period in the .eth registrar.
An Emancipated name can be unwrapped - but when it is wrapped again it will automatically return to the Emancipated state.
How to check:
To check if a name is Emancipated, verify that the PARENT_CANNOT_CONTROL fuse is burned, and the CANNOT_UNWRAP fuse is not.
What happens when it expires:
When the expiration set by the owner of the parent domain is reached, the name expires and moves to the Unregistered state.
Locked
A Locked name provides the assurance that neither the owner of the name nor the owner of any parent name can affect it until it expires.
A name is Locked when the owner of the name revokes their ability to unwrap the name. Only Emancipated names can be Locked, and Locking is a prerequisite for Emancipating subnames. A name can also be optionally locked when a parent emancipates a name, by burning both PARENT_CANNOT_CONTROL and CANNOT_UNWRAP.
Additional permissions over a name can be revoked, such as the ability to create subdomains or set the resolver, and Locking is a prerequisite for revoking these permissions as well.
How to check:
To check if a name is Locked, verify that the CANNOT_UNWRAP fuse is burned.
What happens when it expires:
When expiry is reached, the name moves to the Unregistered state.
State Transitions
Unregistered → Unwrapped
Functions:
- Standard ENS registration via registrar controller
Unwrapped → Wrapped
Functions:
wrap() - for non-.eth names
wrapETH2LD() - for .eth second-level names
onERC721Received() - for .eth names sent to the wrapper
Wrapped → Unwrapped
Functions:
unwrap() - for non-.eth names
unwrapETH2LD() - for .eth names
setSubnodeOwner() with owner set to address(0)
setSubnodeRecord() with owner set to address(0)
setRecord() with owner set to address(0)
Unwrapped/Wrapped → Emancipated
Functions:
registerAndWrapETH2LD() - automatically emancipates .eth names
wrapETH2LD() - automatically emancipates .eth names
setChildFuses() with PARENT_CANNOT_CONTROL burned (from parent owner)
setSubnodeOwner() with fuses including PARENT_CANNOT_CONTROL
setSubnodeRecord() with fuses including PARENT_CANNOT_CONTROL
Emancipated → Locked
Functions:
setFuses() with CANNOT_UNWRAP fuse (requires PARENT_CANNOT_CONTROL already burned)
setChildFuses() with both PARENT_CANNOT_CONTROL and CANNOT_UNWRAP fuses
Emancipated → Unwrapped
Functions:
unwrap() - for non-.eth names (only if not locked)
unwrapETH2LD() - for .eth names (only if not locked)
Any State → Unregistered
Conditions:
- Name expires (only for Emancipated or Locked names)
Expiry Behavior
The NameWrapper tracks an expiration time for each name. Expiry of names is not enabled by default. This means you can leave expiry at 0 or a number less than block.timestamp and the name will have an owner and be able to set records. Expiry causes fuses to reset for any wrapped name. However the name itself will only expire if it is in the Emancipated or Locked state.
For all wrapped names when expiry is reached:
- The NameWrapper treats all fuses as unset and returns
uint32(0) from getData() for fuses.
For Emancipated or Locked names when expiry is reached:
- The NameWrapper returns
address(0) as the name owner from ownerOf() or getData().
For Wrapped (but not Emancipated or Locked) names:
The expiry will only cause parent-controlled fuses to reset, and otherwise has no practical effect on the name.
For .eth names:
.eth names derive their expiry from the .eth registrar; the wrapper’s expiry is set to the end of the name’s grace period. A name that is extended using the Name Wrapper aware .eth registrar controller calling renew() or wrapped using wrapETH2LD(), the name’s expiry will sync the wrapper expiry to the .eth registrar expiry.
At the expiry date, the .eth name will be frozen for the entirety of the grace period. This includes all functionality that checks the owner, but does not affect its subdomains.
If the name is renewed by a wrapper unaware .eth registrar controller, the wrapper expiry of the name will remain in the same expired state and will not sync.
Extending Expiry
Expiry can be extended using the following functions:
setChildFuses() - Called by parent owner
setSubnodeOwner() - Called by parent owner when creating/updating subdomains
setSubnodeRecord() - Called by parent owner when creating/updating subdomains
renew() - Called by registrar controller for .eth names
extendExpiry() - Called by name owner, parent owner, or approved address
Function-specific restrictions:
setChildFuses() and renew():
- No direct restrictions around when they can be called to extend expiry
renew() cannot be called on a name that has expired (past grace period) on the .eth registrar and must be re-registered instead
setSubnodeOwner() and setSubnodeRecord():
- Both revert when the subdomain is Emancipated or Locked
renew():
- Indirectly extends the expiry of a .eth name by renewing the name inside the .eth registrar
extendExpiry():
- Can only be called by the owner of the name, an approved address or the owner of the parent name
- When called by the owner of the name, the
CAN_EXTEND_EXPIRY fuse must have already been burned by the parent
- An approved address can be set by calling
approve()
- Expiry can only be extended, never reduced
- Max expiry is automatically set to the expiry of the parent node