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.
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:
- If the factory address is
address(0), returnmsg.senderunchanged. - Otherwise call
HCA_FACTORY.getAccountOwner(msg.sender). - If the result is
address(0)— the caller is not a registered HCA — returnmsg.sender. - Otherwise return the 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:
HCAContextextendsContext— used by non-upgradeable contracts.HCAContextUpgradeableextendsContextUpgradeable— 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, 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
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.senderfor 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.