diff --git a/src/pages/contracts/ensv2/enhanced-access-control.mdx b/src/pages/contracts/ensv2/enhanced-access-control.mdx
new file mode 100644
index 00000000..399a0f48
--- /dev/null
+++ b/src/pages/contracts/ensv2/enhanced-access-control.mdx
@@ -0,0 +1,178 @@
+---
+description: Enhanced Access Control
+---
+
+# Enhanced Access Control
+
+Enhanced Access Control (EAC) is the permission system used throughout ENSv2. It controls who is allowed to do what - and on which names.
+
+Think of it like a building where each room has its own set of locks: you can give someone a key to one room, or a master key that opens every room. EAC works the same way, but with ENS names and on-chain permissions.
+
+:::note
+The contracts and interfaces described here are **not yet final** and may change prior to mainnet deployment.
+:::
+
+## Resources
+
+A **resource** is the thing you're controlling access to. In most ENS contracts, a resource is a name - but it can be any `uint256` identifier that makes sense for the contract.
+
+For example, the ENS registry treats each name as a separate resource. Permissions you grant on `alice.eth` don't affect `bob.eth` - they're independent.
+
+There's also a special resource called **`ROOT_RESOURCE`** (`0x0`) that represents the contract itself. Permissions granted on `ROOT_RESOURCE` apply everywhere - like a master key. If you have a role on `ROOT_RESOURCE`, you automatically have that role on every individual resource too.
+
+```mermaid
+graph TD
+ ROOT["ROOT_RESOURCE (0x0)\nApplies to all names"]
+ A["alice.eth"]
+ B["bob.eth"]
+ C["carol.eth"]
+ ROOT -.->|"master key"| A
+ ROOT -.->|"master key"| B
+ ROOT -.->|"master key"| C
+```
+
+## Roles
+
+A **role** represents a specific permission - for example, "can set the resolver" or "can register subnames". Each ENS contract defines the roles that are relevant to it.
+
+Roles are always tied to a resource. Granting someone the "set resolver" role on `alice.eth` doesn't let them set the resolver on `bob.eth`. To give someone a permission across all names, grant the role on `ROOT_RESOURCE` instead.
+
+Up to 15 accounts can hold the same role on the same resource, enabling shared management and delegation.
+
+**How role checks work:** when a contract checks whether an account has a role on a specific name, it looks in two places - the name itself and `ROOT_RESOURCE` - and allows the action if the role is found in either. This is how the master key effect works.
+
+The account being checked is the sender as resolved by [Hidden Contract Accounts](/contracts/ensv2/hca), not raw `msg.sender` — smart-account proxies are transparently mapped to their owner before the role lookup runs.
+
+## Admin Roles
+
+Every role has a corresponding **admin role** that controls who can manage it. If you hold the admin role, you can:
+
+- **Grant** the regular role to other accounts
+- **Grant** the admin role itself to other accounts
+- **Revoke** either role from other accounts
+
+```mermaid
+graph LR
+ Admin["Admin Role\n(manages access)"]
+ Regular["Regular Role\n(performs actions)"]
+ Admin -->|"grant / revoke"| Regular
+ Admin -->|"grant / revoke"| Admin
+```
+
+For example, the admin role for "set resolver" controls who is allowed to grant or revoke the "set resolver" permission. Admin roles follow the same resource-scoping - you can be an admin for a specific name or for all names via `ROOT_RESOURCE`.
+
+## Granting and Revoking
+
+Roles are managed through four functions:
+
+- `grantRoles` / `revokeRoles` — manage roles on a specific resource (name)
+- `grantRootRoles` / `revokeRootRoles` — manage roles on `ROOT_RESOURCE` (contract-wide)
+
+The caller must hold the admin role for every role being granted or revoked. Admin role holders can also revoke the admin role itself — including from themselves.
+
+As a safety guardrail, `grantRoles` and `revokeRoles` reject `ROOT_RESOURCE` — you must use `grantRootRoles` / `revokeRootRoles` explicitly for contract-wide permissions. This prevents accidental global grants.
+
+All four functions return `true` if the account's roles actually changed, or `false` if the roles were already in the desired state.
+
+## Callback Hooks
+
+EAC provides four internal hooks that contracts can override to customize permission behavior:
+
+### Role Change Callbacks
+
+- **`_onRolesGranted(resource, account, oldRoles, newRoles, roleBitmap)`** — called after roles are successfully granted. Receives the account's role bitmap before and after the change, plus the specific roles that were newly added.
+- **`_onRolesRevoked(resource, account, oldRoles, newRoles, roleBitmap)`** — called after roles are successfully revoked. Same parameters, with `roleBitmap` containing the roles that were actually removed.
+
+Both are no-ops in the base implementation. The [Permissioned Registry](/contracts/ensv2/permissioned-registry) overrides these to regenerate ERC1155 tokens when roles change, invalidating stale transfer approvals.
+
+### Grant/Revoke Restriction Hooks
+
+- **`_getSettableRoles(resource, account)`** — returns which roles the account is allowed to grant on a given resource. By default, an account can grant any role for which it holds the corresponding admin role. Contracts override this to impose additional restrictions.
+- **`_getRevokableRoles(resource, account)`** — returns which roles the account is allowed to revoke on a given resource. Same default behavior as settable roles.
+
+The [Permissioned Registry](/contracts/ensv2/permissioned-registry) overrides both to prevent admin role escalation on individual names after registration and to block all role changes on unregistered or reserved names.
+
+## Bitmap Layout
+
+Under the hood, roles are packed into a single `uint256` bitmap split into two halves. Each role occupies one nybble (4 bits), giving space for up to 32 regular roles and 32 corresponding admin roles:
+
+```
+ 255 128 127 0
+ ┌──────────────┬───────────────┐
+ │ Admin Roles │ Regular Roles │
+ └──────────────┴───────────────┘
+```
+
+A regular role at nybble index `N` occupies bits `N*4` to `N*4+3`. Its admin counterpart sits at the same position in the upper half (`N*4+128` to `N*4+131`). The same nybble-per-role layout is used for assignee counting - each nybble tracks how many accounts hold that role within a resource (which is why the maximum is 15, the largest value a 4-bit nybble can store).
+
+## Replacing Fuses
+
+EAC replaces the one-way [fuse system](/wrapper/fuses) from the Name Wrapper. Key advantages:
+
+- **Reversible**: roles can be granted and revoked, not just permanently burned
+- **Multi-account**: up to 15 accounts can hold the same role per resource, enabling delegation and shared management
+- **Granular scoping**: permissions can be set contract-wide or per individual resource, with the two scopes composing automatically
+- **Extensible**: each contract defines its own roles - no fixed set of permissions baked into the protocol
+
+## Permissioned Registry Roles
+
+The [Permissioned Registry](/contracts/ensv2/permissioned-registry) defines the following roles for managing name ownership and lifecycle:
+
+| Role | Scope | Purpose |
+| ------------------------- | ------------------------- | ------------------------------------ |
+| `ROLE_REGISTRAR` | root | Register or reserve names |
+| `ROLE_REGISTER_RESERVED` | root | Promote reserved names to registered |
+| `ROLE_SET_PARENT` | root | Set parent registry |
+| `ROLE_UNREGISTER` | root or name | Unregister names |
+| `ROLE_RENEW` | root or name | Extend expiry |
+| `ROLE_SET_SUBREGISTRY` | root or name | Set child registry |
+| `ROLE_SET_RESOLVER` | root or name | Set resolver |
+| `ROLE_CAN_TRANSFER_ADMIN` | root or name (admin only) | Authorize token transfers |
+| `ROLE_UPGRADE` | root | Authorize proxy upgrades |
+
+"Root" scope means the role operates on `ROOT_RESOURCE` only. "Root or name" means it can be granted on either scope, and the two compose — a root grant applies to all names.
+
+Admin roles on individual names can only be granted at registration time. They can be revoked afterward but not re-granted. On `ROOT_RESOURCE`, admin roles work normally. This prevents a name owner from escalating their own permissions after registration.
+
+## Permissioned Resolver Roles
+
+The Permissioned Resolver is the per-name resolver used in ENSv2. Each name gets its own resolver instance deployed via a factory, and all permissions are managed through EAC.
+
+| Role | Scope | Purpose |
+| ---------------------- | ------------ | --------------------------------------------- |
+| `ROLE_SET_ADDR` | root or name | Set address records (e.g. ETH, BTC addresses) |
+| `ROLE_SET_TEXT` | root or name | Set text records (e.g. avatar, email) |
+| `ROLE_SET_CONTENTHASH` | root or name | Set contenthash record |
+| `ROLE_SET_PUBKEY` | root or name | Set SECP256k1 public key record |
+| `ROLE_SET_ABI` | root or name | Set ABI data |
+| `ROLE_SET_INTERFACE` | root or name | Set interface implementer record |
+| `ROLE_SET_NAME` | root or name | Set reverse name record |
+| `ROLE_SET_ALIAS` | root | Set alias targets for name rewriting |
+| `ROLE_CLEAR` | root or name | Clear all records for a node (version bump) |
+| `ROLE_UPGRADE` | root | Authorize proxy upgrades |
+
+### Fine-Grained Permissions
+
+`ROLE_SET_ADDR` and `ROLE_SET_TEXT` support an additional level of scoping beyond name-level. Permissions can be restricted to a specific coin type or text key using a **part** identifier, creating a two-dimensional permission grid:
+
+| | Any record | Specific record |
+| ----------------- | ----------------------- | -------------------------- |
+| **Any name** | `resource(0, 0)` | `resource(0, part)` |
+| **Specific name** | `resource(namehash, 0)` | `resource(namehash, part)` |
+
+When checking permissions, the resolver looks across all four resource combinations and allows the action if any of them grant the required role.
+
+Because of this part-based scoping, the standard `grantRoles()` function is disabled on the Permissioned Resolver. Instead, three specialized functions are provided:
+
+- **`grantNameRoles(name, roleBitmap, account)`** — grant roles scoped to a name (no part restriction)
+- **`grantTextRoles(name, key, account)`** — grant `ROLE_SET_TEXT` for a specific text key on a name
+- **`grantAddrRoles(name, coinType, account)`** — grant `ROLE_SET_ADDR` for a specific coin type on a name
+
+For example, you could grant someone permission to set only the `avatar` text record on `alice.eth`, without giving them access to any other records.
+
+### Aliasing
+
+The Permissioned Resolver supports internal name aliasing via `setAlias()` (requires `ROLE_SET_ALIAS`). An alias maps one name to another using longest-match suffix rewriting:
+
+- `setAlias("a.eth", "b.eth")` causes `sub.a.eth` to resolve using records from `sub.b.eth`
+- Aliases are resolved recursively (cycles of length 1 apply once; longer cycles result in out-of-gas)
diff --git a/src/pages/contracts/ensv2/hca.mdx b/src/pages/contracts/ensv2/hca.mdx
new file mode 100644
index 00000000..813820de
--- /dev/null
+++ b/src/pages/contracts/ensv2/hca.mdx
@@ -0,0 +1,83 @@
+---
+description: Hidden Contract Accounts
+---
+
+# Hidden Contract Accounts
+
+Hidden Contract Accounts (HCAs) let smart-account proxies act on behalf of their owners while still being attributed to the owner. ENSv2 wires HCA-aware sender resolution into every layer of the stack — when a user controls a name through an HCA proxy, the protocol records the owner as the actor for permission checks, ownership reads, and event indexing.
+
+:::note
+The contracts and interfaces described here are **not yet final** and may change prior to mainnet deployment.
+:::
+
+## The Problem
+
+Smart-account flows usually involve a per-user proxy contract that signs and submits transactions on the user's behalf. Without intervention, every contract that inspects `msg.sender` would see the proxy address — not the controlling account. For ENS that means role checks would fail, ownership would land on the proxy, and indexers would track the wrong actor.
+
+## Resolution Mechanism
+
+Every HCA-aware contract inherits `HCAEquivalence`, which carries an immutable reference to an `IHCAFactoryBasic`. When `_msgSender()` is called:
+
+1. If the factory address is `address(0)`, return `msg.sender` unchanged.
+2. Otherwise call `HCA_FACTORY.getAccountOwner(msg.sender)`.
+3. If the result is `address(0)` — the caller is not a registered HCA — return `msg.sender`.
+4. Otherwise return the resolved owner.
+
+```mermaid
+flowchart TD
+ Call["_msgSender() called"] --> Factory{"HCA_FACTORY
== address(0)?"}
+ Factory -- yes --> Sender["return msg.sender"]
+ Factory -- no --> Lookup["HCA_FACTORY.getAccountOwner(msg.sender)"]
+ Lookup --> Check{"result == 0?"}
+ Check -- yes --> Sender
+ Check -- no --> Owner["return resolved owner"]
+```
+
+The factory itself is treated as opaque — the protocol does not care how the owner-account mapping is implemented, only that it can ask.
+
+## Two Context Flavours
+
+OpenZeppelin contracts inherit from one of two `Context` base classes. ENSv2 ships HCA-aware drop-in replacements for both:
+
+- **`HCAContext`** extends `Context` — used by non-upgradeable contracts.
+- **`HCAContextUpgradeable`** extends `ContextUpgradeable` — used by UUPS proxies, e.g. `PermissionedResolver`.
+
+Inheriting either makes every downstream `_msgSender()` call HCA-aware, including the role-check modifiers in [Enhanced Access Control](/contracts/ensv2/enhanced-access-control), `Ownable`, and standard ERC-1155 operator/approval logic.
+
+## Where HCA Is Wired
+
+HCA-aware sender resolution is built into every contract that gates actions or records actors:
+
+| Contract | Reaches HCA via |
+| ------------------------- | ---------------------------------------------- |
+| `ERC1155Singleton` | `HCAContext` (registries inherit transitively) |
+| `PermissionedRegistry` | via `ERC1155Singleton` |
+| `WrapperRegistry` | via `PermissionedRegistry` |
+| `PermissionedResolver` | `HCAContextUpgradeable` |
+| `ETHRegistrar` | `HCAEquivalence` directly |
+| `SimpleRegistryMetadata` | `HCAEquivalence` directly |
+| `BaseUriRegistryMetadata` | `HCAEquivalence` directly |
+
+The HCA factory address is supplied to each contract at construction time.
+
+## The Factory Interface
+
+```solidity
+interface IHCAFactoryBasic {
+ function getAccountOwner(address hca) external view returns (address);
+}
+```
+
+A single read-only method. It returns the owner of `hca` if the address is a registered Hidden Contract Account, or `address(0)` otherwise. The interface selector is `0x442b172c`.
+
+## What HCA Does Not Change
+
+- `tx.origin` — untouched.
+- The actual `msg.sender` for low-level calls and any third-party contracts.
+- Balances, approvals, or behavior on contracts outside the ENSv2 stack.
+
+HCA is a contract-side opt-in for attribution only. It changes who the protocol _thinks_ is acting; it does not change who the EVM thinks is acting.
+
+## Production Factory
+
+The production HCA factory is deployed externally — currently from Rhinestone's ENS modules — and is audited separately. The protocol is implementation-agnostic: passing `address(0)` for the factory disables HCA entirely, which is what test fixtures and devnet deployments do by default.
diff --git a/src/pages/contracts/ensv2/overview.mdx b/src/pages/contracts/ensv2/overview.mdx
index d5c58a90..284237d0 100644
--- a/src/pages/contracts/ensv2/overview.mdx
+++ b/src/pages/contracts/ensv2/overview.mdx
@@ -23,13 +23,13 @@ The information on this page is a work-in-progress. Expect updates as the design
the ownership and transfer rules for names and subnames.
- **Permissions as Standard** - All of the functionality enabled by the [Name Wrapper
in ENSv1](/wrapper/overview) has been integrated into the core of ENSv2, using a new, highly flexible,
- role-based permissions model.
+ [role-based permissions model](/contracts/ensv2/enhanced-access-control).
- **No Grace Period** - The grace period is no longer a part of the ENS protocol, meaning names expire when they say they expire. Upon expiration, names immediately enter into the [temporary premium](/registry/eth#premium--auctions) period. The owner of a recently-expired name doesn't have to pay the premium fee during this period.
- **Contract Factories** - Rather than relying on large multi-user contracts such as
the default public resolver, every name gets its own resolver, and every name
with subnames gets its own registry contract. This simplifies the contracts significantly, while offering users
new functionality such as the possibility of 'aliasing' multiple domains to
- the same records.
+ the same records. See [Verifiable Factory](/contracts/ensv2/verifiable-factory) for the deployment mechanism.
## What hasn't changed?
@@ -161,7 +161,7 @@ at all!
### Token Representation and permissions
Although it's not required by the ENSv2 registry interface specification, typically registries are
-implemented as ERC1155 token contracts, and standardized tooling is likely to expect as much. Each registry
+implemented as `ERC1155` token contracts, and standardized tooling is likely to expect as much. Each registry
has an overall owner, who is responsible for the registry itself, as well as owners for individual subnames,
which are represented as tokens. Because of the hierarchical nature of the registry structure as demonstrated
above, merely owning a subname token on a registry does not guarantee anything in isolation: to be meaningful,
diff --git a/src/pages/contracts/ensv2/permissioned-registry.mdx b/src/pages/contracts/ensv2/permissioned-registry.mdx
new file mode 100644
index 00000000..e6f7adf0
--- /dev/null
+++ b/src/pages/contracts/ensv2/permissioned-registry.mdx
@@ -0,0 +1,158 @@
+---
+description: Permissioned Registry
+---
+
+# Permissioned Registry
+
+The Permissioned Registry is the tokenized registry at the heart of ENSv2 name management. Each registered name becomes an `ERC1155` token with exactly one owner, and all permissions are managed through [Enhanced Access Control](/contracts/ensv2/enhanced-access-control).
+
+:::note
+The contracts and interfaces described here are **not yet final** and may change prior to mainnet deployment.
+:::
+
+## Names
+
+Each name in the registry is identified by its **labelhash** (the `keccak256` hash of the label string) and stores:
+
+- **Subregistry**: pointer to a child registry (for managing subnames)
+- **Resolver**: address of the resolver contract
+- **Expiry**: timestamp after which the name is considered expired (`block.timestamp >= expiry`)
+- **Versioning**: internal counters that isolate permissions between registrations and prevent stale token approvals (see [Versioning](#versioning))
+
+## Name Lifecycle
+
+Names exist in one of three states:
+
+```mermaid
+stateDiagram-v2
+ [*] --> AVAILABLE
+ AVAILABLE --> REGISTERED: register()
+ AVAILABLE --> RESERVED: register() with owner=0
+ RESERVED --> REGISTERED: register()
+ REGISTERED --> AVAILABLE: unregister() or expiry
+ RESERVED --> AVAILABLE: unregister() or expiry
+```
+
+- `AVAILABLE`: never registered or expired. Open for registration.
+- `RESERVED`: placeholder with no owner and no token. Useful for pre-allocating names before assigning them.
+- `REGISTERED`: has an owner, a token, and active permissions.
+
+**State transitions:**
+
+| From | To | Required role | [Scope](/contracts/ensv2/enhanced-access-control#resources) |
+| --------------------- | ---------- | ------------------------ | ----------------------------------------------------------- |
+| AVAILABLE | REGISTERED | `ROLE_REGISTRAR` | root |
+| AVAILABLE | RESERVED | `ROLE_REGISTRAR` | root |
+| RESERVED | REGISTERED | `ROLE_REGISTER_RESERVED` | root |
+| REGISTERED / RESERVED | AVAILABLE | `ROLE_UNREGISTER` | root or name |
+
+### Registration
+
+`register()` accepts a `label` (string), `owner`, `registry` (subregistry), `resolver`, `roleBitmap` (initial roles granted to the owner), and `expiry`. Labels are validated for size before registration. If `owner` is `address(0)`, the name is reserved instead of registered, and `roleBitmap` must be `0`.
+
+A non-expired registered name cannot be re-registered directly — it must be unregistered first. Similarly, a reserved name cannot be re-reserved; it can only be promoted to registered.
+
+When promoting a `RESERVED` name to `REGISTERED`, if `expiry` is `0` the current expiry is preserved.
+
+Re-registering an expired name that had a previous owner burns the old token and increments both version counters, ensuring stale permissions and token approvals don't carry over.
+
+### Unregistration
+
+`unregister()` sets the name's expiry to `block.timestamp`, making it immediately available. If the name was `REGISTERED` (has an owner), the token is burned and both version counters are incremented.
+
+### Renewal
+
+`renew()` extends a name's expiry but cannot reduce it. Both `REGISTERED` and `RESERVED` names can be renewed. Expired names cannot be renewed — they must be re-registered.
+
+## anyId Polymorphism
+
+Most functions accept an `anyId` parameter that can be a `labelhash`, `tokenId`, or `resource` interchangeably. Internally, `_entry()` zeroes the version bits to find the canonical storage slot for the name. This means you can pass whichever identifier you have on hand — the registry resolves it to the same underlying entry.
+
+This applies to `setSubregistry()`, `setResolver()`, `renew()`, `unregister()`, `getExpiry()`, `getStatus()`, `getState()`, `getTokenId()`, `getResource()`, and all EAC role functions (`grantRoles()`, `revokeRoles()`, `roles()`, etc.).
+
+## Ownership
+
+Each registered name is an `ERC1155` token with exactly one owner (singleton, not fungible). The token ID changes when the name is re-registered or when roles change (see [Versioning](#versioning)).
+
+`ownerOf()` returns `address(0)` for:
+
+- Expired names — ownership is time-bounded
+- Stale token IDs — after versioning changes, old token IDs are no longer valid
+
+`latestOwnerOf()` returns the owner regardless of expiry or version staleness — useful for historical queries or determining who last held a name.
+
+Operator approvals and transfer checks resolve the caller through [Hidden Contract Accounts](/contracts/ensv2/hca), so smart-account proxies are attributed to their owner.
+
+## Token Metadata
+
+The registry exposes ERC-1155 `uri(tokenId)` by delegating to a swappable [Registry Metadata](/contracts/ensv2/registry-metadata) provider set at construction. The provider decides the URI format — per-token URIs, a single shared base URI, or any custom strategy.
+
+## Roles
+
+All roles use [Enhanced Access Control](/contracts/ensv2/enhanced-access-control) mechanics for role-based access control.
+
+| Role | Scope | Purpose |
+| ------------------------- | ------------------------- | ------------------------------------ |
+| `ROLE_REGISTRAR` | root | Register or reserve names |
+| `ROLE_REGISTER_RESERVED` | root | Promote reserved names to registered |
+| `ROLE_SET_PARENT` | root | Set parent registry |
+| `ROLE_UNREGISTER` | root or name | Unregister names |
+| `ROLE_RENEW` | root or name | Extend expiry |
+| `ROLE_SET_SUBREGISTRY` | root or name | Set child registry |
+| `ROLE_SET_RESOLVER` | root or name | Set resolver |
+| `ROLE_CAN_TRANSFER_ADMIN` | root or name (admin only) | Authorize token transfers |
+| `ROLE_UPGRADE` | root | Authorize proxy upgrades |
+
+"Root" scope means the role only works on `ROOT_RESOURCE`. "Root or name" means it can be granted on either scope, and the two compose — a root grant applies to all names.
+
+**Admin role restriction on names:** admin roles on individual names can only be granted at registration time. They can be revoked afterward but not re-granted. On `ROOT_RESOURCE`, admin roles work normally. This prevents a name owner from escalating their own permissions after registration.
+
+## Transfers
+
+Transferring a name's token requires `ROLE_CAN_TRANSFER_ADMIN` as an **admin role on the token owner** (not the operator — operator approval via `ERC1155` is a separate check).
+
+When a token transfers, all roles are atomically moved from the old owner to the new owner — the old owner's roles are revoked first (freeing assignee slots), then granted to the new owner. Roles granted to other accounts on the same name are unaffected. Without `ROLE_CAN_TRANSFER_ADMIN`, the name is effectively non-transferable — similar to the `CANNOT_TRANSFER` fuse in the Name Wrapper.
+
+## Versioning
+
+The registry uses internal versioning to provide two security guarantees:
+
+**Permission isolation across registrations** — when a name is unregistered and later re-registered, all roles from the previous registration are invalidated. The new owner starts with a clean permission scope, preventing any stale grants from carrying over.
+
+**Preventing transfer griefing** — when roles are granted or revoked via `grantRoles()` / `revokeRoles()`, the token ID changes (the owner stays the same but receives a new token via a burn+mint cycle). This invalidates any pending `ERC1155` transfer approvals tied to the old token ID, preventing an attack where someone approves a transfer and then has their roles revoked — without this, the approved party could race to transfer the token before the revocation takes effect. During normal transfers, roles move to the new owner without changing the token ID.
+
+**Expiry enforcement** — expired names retain their permissions, but all mutating operations check expiry first and revert if the name has lapsed, preventing stale permissions from being exercised. On re-registration, version counters increment to create a fresh scope that invalidates any prior grants.
+
+## EAC Hook Overrides
+
+The Permissioned Registry overrides several [EAC callback hooks](/contracts/ensv2/enhanced-access-control#callback-hooks) to enforce registry-specific invariants:
+
+**Token regeneration on role changes** — when roles are granted or revoked via `grantRoles()` / `revokeRoles()`, the `_onRolesGranted` and `_onRolesRevoked` hooks trigger a token regeneration (burn + mint with a new token ID). This invalidates any pending ERC1155 transfer approvals tied to the old token ID, preventing an attacker from racing to transfer a token after their roles have been revoked.
+
+**Admin role restriction on names** — `_getSettableRoles` is overridden so that admin roles on individual names can only be assigned at registration time. After registration, only regular (non-admin) roles can be granted on a name. On `ROOT_RESOURCE`, admin roles work normally. This prevents a name owner from escalating their own permissions after registration.
+
+**No role changes on unregistered names** — both `_getSettableRoles` and `_getRevokableRoles` return `0` for names that are `AVAILABLE` or `RESERVED`, blocking all role operations until the name is fully registered.
+
+## Registry Hierarchy
+
+Each name can point to a child registry via its subregistry field. These parent-child relationships form a tree that mirrors the DNS hierarchy:
+
+```
+.eth registry → nick.eth → sub.nick.eth
+(parent) (child) (grandchild)
+```
+
+The registry also stores its own parent via `setParent()` / `getParent()`, which records both the parent registry address and the child label.
+
+`getSubregistry()` and `getResolver()` return `address(0)` for expired names, preventing resolution through lapsed names.
+
+## View Functions
+
+| Function | Returns |
+| ------------------------ | ------------------------------------------------------------------ |
+| `getState(anyId)` | Complete state: status, expiry, latest owner, tokenId, resource |
+| `getStatus(anyId)` | `AVAILABLE`, `RESERVED`, or `REGISTERED` |
+| `getExpiry(anyId)` | Expiry timestamp |
+| `getTokenId(anyId)` | Current token ID for the name |
+| `getResource(anyId)` | Current EAC resource ID for the name |
+| `latestOwnerOf(tokenId)` | Owner regardless of expiry/version (useful for historical queries) |
diff --git a/src/pages/contracts/ensv2/registry-metadata.mdx b/src/pages/contracts/ensv2/registry-metadata.mdx
new file mode 100644
index 00000000..6d4a5676
--- /dev/null
+++ b/src/pages/contracts/ensv2/registry-metadata.mdx
@@ -0,0 +1,85 @@
+---
+description: Registry Metadata
+---
+
+# Registry Metadata
+
+Registries in ENSv2 delegate ERC-1155 metadata to a swappable provider. The registry holds ownership state; the provider decides what `uri()` returns. This separates presentation concerns (image, name, description for marketplaces and indexers) from ownership logic, and lets a single registry serve many metadata strategies.
+
+:::note
+The contracts and interfaces described here are **not yet final** and may change prior to mainnet deployment.
+:::
+
+## How the Pieces Fit
+
+Three contracts cooperate to produce a token URI:
+
+- **`IRegistryMetadata`** — the provider interface, with a single `tokenUri(uint256) → string` method. Interface selector `0x1675f455`.
+- **`MetadataMixin`** — base mixin holding an immutable `METADATA_PROVIDER` address. Its internal `_tokenURI(tokenId)` forwards to the provider, or returns an empty string if the provider is `address(0)`.
+- **Registry contract** — inherits `MetadataMixin` and overrides ERC-1155 `uri(tokenId)` to return `_tokenURI(tokenId)`.
+
+```mermaid
+flowchart LR
+ Caller["uri(tokenId)"] --> Registry["Registry
(MetadataMixin)"]
+ Registry --> Provider["IRegistryMetadata
provider"]
+ Provider --> URI["string URI"]
+```
+
+The provider is set once at registry construction and is immutable thereafter. To change the URI strategy you upgrade or replace the registry, or build a provider that itself looks up data from another contract.
+
+## Stock Implementations
+
+Two providers ship with the protocol. Both inherit [Enhanced Access Control](/contracts/ensv2/enhanced-access-control) and gate updates with a single role.
+
+### `SimpleRegistryMetadata`
+
+Stores a distinct URI per token in a `mapping(uint256 => string)`.
+
+```solidity
+function setTokenUri(uint256 tokenId, string calldata uri) external;
+function tokenUri(uint256 tokenId) external view returns (string memory);
+```
+
+`setTokenUri` is gated by `_ROLE_UPDATE_METADATA` (bit 0) on the `ROOT_RESOURCE`. Use this when each name needs its own metadata payload — for example, per-name SVG art or off-chain JSON URIs.
+
+### `BaseUriRegistryMetadata`
+
+Returns a single shared base URI for every token; the `tokenId` argument is ignored.
+
+```solidity
+function setTokenBaseUri(string calldata uri) external;
+function tokenUri(uint256 /* tokenId */) external view returns (string memory);
+```
+
+Same role and scope as above. Use this when a single template URI (e.g. `https://example.com/metadata/{id}.json` resolved by the indexer) is enough.
+
+## The Metadata Update Role
+
+Each metadata contract runs its own EAC instance, separate from the registry's. The role bit is local — granting `_ROLE_UPDATE_METADATA` on a metadata contract does not give the holder any role on the registry, and vice versa. At deployment, all roles on the metadata contract are granted to the deployer.
+
+| Role | Bit | Scope | Purpose |
+| ----------------------------- | --- | --------- | ------------------------------- |
+| `_ROLE_UPDATE_METADATA` | 0 | root only | Update token URI(s) |
+| `_ROLE_UPDATE_METADATA_ADMIN` | 128 | root only | Grant or revoke the update role |
+
+## Building a Custom Provider
+
+Implement `IRegistryMetadata` and pass the address to the registry constructor:
+
+```solidity
+contract MyMetadata is IRegistryMetadata {
+ function tokenUri(uint256 tokenId) external view returns (string memory) {
+ // compose SVG, fetch from another contract, derive from on-chain state, etc.
+ }
+}
+```
+
+Common patterns:
+
+- Deterministic SVG composition from on-chain state (no off-chain dependency).
+- URL templates parameterised by token ID and resolved by an indexer.
+- A registry of registries: a single provider that routes by token ID range.
+
+## Where the URI Ends Up
+
+The provider's output is returned via the registry's standard ERC-1155 `uri()`. Indexers, marketplaces, and the ENS metadata service pick it up from there. Wallets that ignore subname tokens may not query it at all — that is a client concern, not a registry concern.
diff --git a/src/pages/contracts/ensv2/verifiable-factory.mdx b/src/pages/contracts/ensv2/verifiable-factory.mdx
new file mode 100644
index 00000000..e33307c8
--- /dev/null
+++ b/src/pages/contracts/ensv2/verifiable-factory.mdx
@@ -0,0 +1,130 @@
+---
+description: Verifiable Factory
+---
+
+# Verifiable Factory
+
+ENSv2 deploys per-name resolvers and per-name subname registries through a single shared factory. Each instance is a UUPS proxy with a deterministic CREATE2 address and an on-chain proof of provenance. This page covers the factory itself and how the protocol uses it.
+
+:::note
+The contracts and interfaces described here are **not yet final** and may change prior to mainnet deployment.
+:::
+
+## Why a Factory
+
+ENSv2 chooses per-name instances over a single shared resolver or registry contract (see the "Contract Factories" point in the [Overview](/contracts/ensv2/overview#whats-new-in-ensv2)). Each instance is a UUPS proxy, so deployment is cheap and any single instance can be upgraded without touching the rest of the system. The factory makes every such deployment:
+
+- **Deterministic** — the address is known before deployment.
+- **Verifiable** — anyone can prove on-chain that an arbitrary address really came from this factory with a known proxy bytecode.
+- **Cheap** — only the proxy is deployed; the implementation is shared.
+
+## Deterministic Deployment
+
+```solidity
+function deployProxy(
+ address implementation,
+ uint256 salt,
+ bytes memory data
+) external returns (address proxy);
+```
+
+`deployProxy` uses CREATE2 to deploy a `UUPSProxy` with `outerSalt = keccak256(abi.encode(msg.sender, salt))`. After construction, the factory calls `proxy.initialize(implementation, data)` to point the proxy at the implementation and forward `data` as initialisation calldata.
+
+Because `outerSalt` mixes in `msg.sender`, the same `salt` value submitted by two different deployers produces two different proxy addresses — there is no contention between callers and no need for global salt coordination.
+
+```mermaid
+sequenceDiagram
+ participant Caller
+ participant Factory as VerifiableFactory
+ participant Proxy as UUPSProxy
+ participant Impl as Implementation
+
+ Caller->>Factory: deployProxy(impl, salt, data)
+ Factory->>Factory: outerSalt = keccak256(sender, salt)
+ Factory->>Proxy: CREATE2 with outerSalt
+ Proxy->>Proxy: store factory & salt
+ Factory->>Proxy: initialize(impl, data)
+ Proxy->>Impl: delegatecall(data)
+ Factory-->>Caller: proxy address
+```
+
+## The UUPSProxy
+
+Each proxy stores two pieces of provenance:
+
+- An immutable `verifiableProxyFactory` address baked into the proxy bytecode at construction.
+- A salt stored at the ERC-7201 namespaced slot `eth.ens.proxy.verifiable.salt`, exposed via `getVerifiableProxySalt()`.
+
+`initialize(implementation, data)` runs once. It calls OpenZeppelin's `ERC1967Utils.upgradeToAndCall`, which sets the implementation slot and delegate-calls the implementation with `data` so it can run its own initializer.
+
+## On-chain Verification
+
+```solidity
+function verifyContract(address proxy) external view returns (bool);
+```
+
+`verifyContract` asks the proxy for its salt, reconstructs the expected CREATE2 address from `(UUPSProxy creation code, factory, salt)`, and returns `true` if it matches `proxy`. This is the property that makes the factory **verifiable**: a smart contract can prove that an arbitrary address really is a UUPS proxy deployed by this factory, without trusting metadata or off-chain data.
+
+## How ENSv2 Uses It
+
+Three implementation contracts are deployed once and then proxied per-name through the factory:
+
+| Implementation | Salt scheme | Where it's deployed |
+| -------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------- |
+| `PermissionedResolverImpl` | `keccak256("OwnedResolver", owner, version)` — one resolver per owner | On demand by users |
+| `UserRegistryImpl` | `keccak256("UserRegistry", namehash, version)` — one subname registry per name | On demand by name owners |
+| `WrapperRegistryImpl` | per-name | Inside migration controllers when a locked v1 name is wrapped |
+
+Salt schemes come from `contracts/script/setup.ts` in the namechain repo; the same schemes let any client predict a deployment address before it exists.
+
+## Upgrade Authorization
+
+Each implementation overrides `_authorizeUpgrade`. In ENSv2 the check is uniform — only an account holding `ROLE_UPGRADE` on the proxy's `ROOT_RESOURCE` can upgrade the implementation:
+
+```solidity
+function _authorizeUpgrade(address newImplementation)
+ internal
+ override
+ onlyRootRoles(ROLE_UPGRADE)
+{}
+```
+
+Upgrades target a single proxy, not the shared implementation; an upgrade to one user's resolver or subname registry does not affect anyone else's.
+
+## Computing Addresses Off-chain
+
+Because the proxy address is fully determined by `(factory, deployer, salt, UUPSProxy bytecode)`, clients can compute it before any transaction is sent:
+
+```ts
+import { encodeAbiParameters, getCreate2Address, keccak256 } from 'viem'
+
+function predictProxyAddress({
+ factoryAddress,
+ proxyBytecode,
+ deployer,
+ salt,
+}: {
+ factoryAddress: `0x${string}`
+ proxyBytecode: `0x${string}`
+ deployer: `0x${string}`
+ salt: bigint
+}) {
+ const outerSalt = keccak256(
+ encodeAbiParameters(
+ [{ type: 'address' }, { type: 'uint256' }],
+ [deployer, salt]
+ )
+ )
+ const initCode = `${proxyBytecode}${encodeAbiParameters(
+ [{ type: 'address' }, { type: 'bytes32' }],
+ [factoryAddress, outerSalt]
+ ).slice(2)}` as `0x${string}`
+ return getCreate2Address({
+ from: factoryAddress,
+ salt: outerSalt,
+ bytecodeHash: keccak256(initCode),
+ })
+}
+```
+
+This is the pattern used inside the ENSv2 deployment scripts (`computeVerifiableProxyAddress` in `setup.ts`); reuse it whenever you need a name's resolver or subname-registry address before the user actually creates it.
diff --git a/vocs.config.tsx b/vocs.config.tsx
index 25a64a0d..8ae93eb7 100644
--- a/vocs.config.tsx
+++ b/vocs.config.tsx
@@ -359,6 +359,26 @@ export default defineConfig({
text: 'Overview',
link: '/contracts/ensv2/overview',
},
+ {
+ text: 'Enhanced Access Control',
+ link: '/contracts/ensv2/enhanced-access-control',
+ },
+ {
+ text: 'Permissioned Registry',
+ link: '/contracts/ensv2/permissioned-registry',
+ },
+ {
+ text: 'Registry Metadata',
+ link: '/contracts/ensv2/registry-metadata',
+ },
+ {
+ text: 'Verifiable Factory',
+ link: '/contracts/ensv2/verifiable-factory',
+ },
+ {
+ text: 'Hidden Contract Accounts',
+ link: '/contracts/ensv2/hca',
+ },
// {
// text: 'Registries',
// link: '/contracts/ensv2/registries',