As an NFT collectibles project, continually improving and delivering utility—and more importantly value—to your holders should be your number one priority.

Utility doesn't just have to be big exclusive partnerships or triple-A play-to-earn games that take months or even years to develop. In fact, we believe it should be an on-going process that adds value to your ecosystem and improves the overall experience—lots of little things.

One of those is providing your community with a free ENS subdomain, i.e., paul.tdbc.eth or 5050.bayc.eth. Subdomains function exactly like a full ENS domain and are 100% owned and controlled by its creator.

And the benefits are clear. Your holders get a free ENS name that they can use across Web3, which increases your brands identity and reach-strengthening your marketing efforts.

Implementation is fairly straight forward via a custom smart contract that proxies subdomain registrations to the ENS registry. You could do this entirely off-chain but that wouldn't be very decentralized.

There are a few caveats to note:

  1. NFTs are not minted for ENS subdomains. You can extend the example contract below to include ERC721 support and simply call the _mint_ function as you normally would. ENS even provide a metadata service that will take care of the image generation. We have purposely left this out as it more than doubles gas costs and we wanted to keep the subdomains as "free" as possible to increase adoption. Alternatively, you can airdrop the NFTs for free to your ENS owners via the Polygon network.

  2. You have to transfer ownership of the root ENS domain to the smart contract. We have done this for security reasons, otherwise the owner of the domain can change or update any subdomain records via the ENS app. We have allowed the ENS name to be transferred back out of the contract so it isn't lost forever. You can increase security further by removing the transferDomainOwnership function entirely but you will lose control of your ENS domain forever.

  3. With the below implementation, users can register as many subdomains as they desire. You might want to limit this to 1 (just add a mapping to track) or be bound to the number of your NFTs they own (via the balanceOf check). You could even determine what token IDs they own and only allow them to register those, i.e., 3456.tdbc.eth.

If you need any assistance in deploying the contract, making changes to better fit your community, or hooking up a front-end then get in touch: jakub@topdogstudios.io

// SPDX-License-Identifier: MIT
// Top Dog Studios ENS Subdomain Mapper v0.1

pragma solidity ^0.8.14;

interface IERC721 {
    function balanceOf(address owner) external view returns (uint256);
}

interface IENSResolver {
	function setAddr(bytes32 node, address addr) external;
	function addr(bytes32 node) external view returns (address);
}

interface IENSRegistry {
	function setOwner(bytes32 node, address owner) external;
	function setSubnodeOwner(bytes32 node, bytes32 label, address owner) external;
	function setResolver(bytes32 node, address resolver) external;
	function owner(bytes32 node) external view returns (address);
	function resolver(bytes32 node) external view returns (address);
}

contract TDBCENSMapper {
    bytes32 private constant EMPTY_NAMEHASH = 0x00;

	address private owner;
    IERC721 private immutable tdbc;
    IERC721 private immutable tcbc;
	IENSRegistry private registry;
	IENSResolver private resolver;
	bool public locked;

	event SubdomainCreated(address indexed creator, address indexed owner, string subdomain, string domain, string topdomain);
	event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
	event RegistryUpdated(address indexed previousRegistry, address indexed newRegistry);
	event ResolverUpdated(address indexed previousResolver, address indexed newResolver);
	event DomainTransfersLocked();

	constructor(IERC721 _topDogs, IERC721 _topCats, IENSRegistry _registry, IENSResolver _resolver) {
		owner = msg.sender;
        tdbc = _topDogs;
        tcbc = _topCats;
		registry = _registry;
		resolver = _resolver;
		locked = false;
	}

	modifier onlyOwner() {
		require(msg.sender == owner);
		_;
	}

	function newSubdomain(string calldata _subdomain, string calldata  _domain, string calldata  _topdomain, address _owner, address _target) external {
		require(tdbc.balanceOf(_owner) > 0 || tcbc.balanceOf(_owner) > 0, "UNAUTHORIZED");

        bytes32 topdomainNamehash = keccak256(abi.encodePacked(EMPTY_NAMEHASH, keccak256(abi.encodePacked(_topdomain))));
		bytes32 domainNamehash = keccak256(abi.encodePacked(topdomainNamehash, keccak256(abi.encodePacked(_domain))));
		require(registry.owner(domainNamehash) == address(this), "INVALID_DOMAIN");

		bytes32 subdomainLabelhash = keccak256(abi.encodePacked(_subdomain));
		bytes32 subdomainNamehash = keccak256(abi.encodePacked(domainNamehash, subdomainLabelhash));
		require(registry.owner(subdomainNamehash) == address(0) || registry.owner(subdomainNamehash) == msg.sender, "SUB_DOMAIN_ALREADY_OWNED");

		registry.setSubnodeOwner(domainNamehash, subdomainLabelhash, address(this));
		registry.setResolver(subdomainNamehash, address(resolver));
		resolver.setAddr(subdomainNamehash, _target);
		registry.setOwner(subdomainNamehash, _owner);

		emit SubdomainCreated(msg.sender, _owner, _subdomain, _domain, _topdomain);
	}

	function domainOwner(string calldata _domain, string calldata _topdomain) external view returns (address) {
		bytes32 topdomainNamehash = keccak256(abi.encodePacked(EMPTY_NAMEHASH, keccak256(abi.encodePacked(_topdomain))));
		bytes32 namehash = keccak256(abi.encodePacked(topdomainNamehash, keccak256(abi.encodePacked(_domain))));

		return registry.owner(namehash);
	}

	function subdomainOwner(string calldata _subdomain, string calldata _domain, string calldata _topdomain) external view returns (address) {
		bytes32 topdomainNamehash = keccak256(abi.encodePacked(EMPTY_NAMEHASH, keccak256(abi.encodePacked(_topdomain))));
		bytes32 domainNamehash = keccak256(abi.encodePacked(topdomainNamehash, keccak256(abi.encodePacked(_domain))));
		bytes32 subdomainNamehash = keccak256(abi.encodePacked(domainNamehash, keccak256(abi.encodePacked(_subdomain))));

		return registry.owner(subdomainNamehash);
	}

    function subdomainTarget(string calldata _subdomain, string calldata _domain, string calldata _topdomain) external view returns (address) {
        bytes32 topdomainNamehash = keccak256(abi.encodePacked(EMPTY_NAMEHASH, keccak256(abi.encodePacked(_topdomain))));
        bytes32 domainNamehash = keccak256(abi.encodePacked(topdomainNamehash, keccak256(abi.encodePacked(_domain))));
        bytes32 subdomainNamehash = keccak256(abi.encodePacked(domainNamehash, keccak256(abi.encodePacked(_subdomain))));
        address currentResolver = registry.resolver(subdomainNamehash);

        return IENSResolver(currentResolver).addr(subdomainNamehash);
    }

	function transferDomainOwnership(bytes32 _node, address _owner) external onlyOwner {
		require(!locked);
		registry.setOwner(_node, _owner);
	}

	function lockDomainOwnershipTransfers() external onlyOwner {
		require(!locked);
		locked = true;
		emit DomainTransfersLocked();
	}

	function updateRegistry(IENSRegistry _registry) external onlyOwner {
		require(registry != _registry, "INVALID_REGISTRY");
		emit RegistryUpdated(address(registry), address(_registry));
		registry = _registry;
	}

	function updateResolver(IENSResolver _resolver) external onlyOwner {
		require(resolver != _resolver, "INVALID_RESOLVER");
		emit ResolverUpdated(address(resolver), address(_resolver));
		resolver = _resolver;
	}
}

Questions? Looking for your perfect launch partner? Reach out to @TopDogStudios or hello@topdogstudios.io.

The Top Dog Team ❤