Skip to main content

EnsMapper Contract

EnsMapper Contract is a custom ENS Resolver contract created by PCC's wizard hodl.pcc.eth. This contract made PCC the first project that links ERC-721 NFTs to ENS subdomains possible.

The contract itself says feel free to reuse it for other projects to use!

Source Code

The EnsMapper contract is deployed to 0x9B6d20F524367D7E98ED849d37Fc662402DCa7FB, and source code is public on Etherscan. Here's a copy of the contract's source code:

View Contract Source Code
EnsMapper.sol
/*
▓▓█
▒██▒▒█
█▓▓▓░▒▓▓
▒█▓▒█░▒▒▒█
▒█▒▒▒█▒▒▒▒▓▒
▓▓▒░ ▓█▒▒▒▓██▓▒░▒█
█▓▓██▓░ ▓█▒▒▒▒████▒▒▒█
▓█▓▒▒▓██▓░ ▒█▒▒▒▒▒██▓█▓░░▓▒
▓▒▓▒▒▒▒▒▓█▓░ ░▒▒▓▓██▒▒▒▒▒▒█████▒▒▒▓
▓░█▒▒▒▒▒▒▒▓▓█▓█▓▓▓▓▒▒▒▒▒▒▒▒██▓██▒░▒█
▓░▓█▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓████▒▒▒█
▓░▓██▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▓██░░░█
▓░▓███▒▒▒▒▒▒▒▒▒▒▒▓█▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▒▓▓
▒▒▒██▓▒▓█▓▒▒▒▒▒▒▒▓▒▒▒▒▒▒▓▓▓▒▒▒▒▒▒▒▓▒█
▓▒█▓▒▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▓█▓▓▓▓█▓▒▒▒▒▒▒▒▓▒
▓▒█▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓ ▓█▓▒▒▒▒▒▓█
▒▒▓▒▒▒▓▓▓▒▒▒▒▒▒▒▒▒▓▓ ░▓▓ █▓▒▒▒▒▒█
█▒▒▓▓▓▒▒▓▓▒▒▒▒▒▒▓▓ █████▓ █▓▒▒▒▒▓▒
▓▓█▒ ▒▓▒▒▒▒▒█ ░██████ ░█▒▒▒▒▓▓
▓█▒ ▒███ ▒▓▒▒▒▒█ ██████ ▓▒▒▒▒▒▓
██ █████ █▒▒▒▒█ ███▓ ▓▓▒▒▒▒▓
█▓ █████ ▒▓▒▒▒█ █▓▓▓▒▒▓
█▓ ░███ ░▓▒▒▒▓█ ░█▓▒▒▒▓▒▓
██ ▒▓▒▒▒▒▓▓ ░▒▓█▓ ░▓▓
▓█░ █▓██▓▒▒▓█▓▓▓▓██▓▓▒▓▒░░▒▓▒▓
▒██░ ▓▒███▓▒▒▒▒▓▓▓▓▒▒▒▒▒▒▓▓▓▓▒▓
█▓█▓▓▒▒▓█▓▒░██▒▒▓▓█▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓█▒
▓ ░▓▓▓▓▓▒▓▓▓▓▒▓▓▓▒▓▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓
▒▒▒▓▒▒▒▒▒▒▓█░ ░░░ ▓▓▒▒▒▒▒▒▒▒▒▒▒▓██▓▒
█▓▒▒▒▒▒▒▒▒▓▓ ░░░ ▓▓▒▒▒▒▒▒▒▒▒▓▓▓▒▒▓▒
██▓▓▒▒▒▒▒▒█▒░░░░█▒▒▒▒▒▒▒▒▓█▓▓▒▒▒▒▒
▒██▓▓▒▒▒▒▒█▓▒▒▓▒▒▒▒▒▒▓███▓▒▒▒▒▒▓▓
░▒▓▓▓▓▒▒▓▓▓▓▓▓████▓▓█▒▒▒▒▒▓▓█░
████████████████████████████████████████████████████████████████████████
█▄─▄▄─█▄─██─▄█▄─▄▄▀█▄─▄▄▀█▄─▀█▄─▄█▄─▄▄─█▄─▄███─▄▄─█▄─▄▄─█▄─▄▄─█─▄▄▄▄████
██─▄▄▄██─██─███─▄─▄██─▄─▄██─█▄▀─███─▄█▀██─██▀█─██─██─▄▄▄██─▄█▀█▄▄▄▄─████
█▄▄▄████▄▄▄▄██▄▄█▄▄█▄▄█▄▄█▄▄▄██▄▄█▄▄▄▄▄█▄▄▄▄▄█▄▄▄▄█▄▄▄███▄▄▄▄▄█▄▄▄▄▄████
████████████████████████████████████████████████████████████████████████
█─▄▄▄─█─▄▄─█▄─██─▄█▄─▀█▄─▄█─▄─▄─█▄─▄▄▀█▄─█─▄███─▄▄▄─█▄─▄███▄─██─▄█▄─▄─▀█
█─███▀█─██─██─██─███─█▄▀─████─████─▄─▄██▄─▄████─███▀██─██▀██─██─███─▄─▀█
█▄▄▄▄▄█▄▄▄▄██▄▄▄▄██▄▄▄██▄▄██▄▄▄██▄▄█▄▄██▄▄▄████▄▄▄▄▄█▄▄▄▄▄██▄▄▄▄██▄▄▄▄██*/

//SPDX-License-Identifier: MIT



//Twitter: @hodl_pcc
//
/* This code should work for most ERC-721 contracts. Please feel free to reuse

- Change IERC721Enumerable contract address in constructor
- Update domainLabel value
- Update nftImageBaseUri value to the base path of the images
- Set controller address of the parent domain to this deployed contract (in ENS web app)
*/

pragma solidity ^0.8.2;

import "@ensdomains/ens-contracts/contracts/registry/ENS.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
}

contract EnsMapper is Ownable {

using Strings for uint256;

ENS private ens;
IERC721Enumerable public nft;
bytes32 public domainHash;
mapping(bytes32 => mapping(string => string)) public texts;

mapping(address => uint256) public nextRegisterTimestamp;

string public domainLabel = "pcc";
string public nftImageBaseUri = "https://ipfs.io/ipfs/QmUfyS4W6cBRRWtWpbW1as4ziaiEHTBD9WVcn1nrEB6xPu/";
bool public useEIP155 = true;

mapping(bytes32 => uint256) public hashToIdMap;
mapping(uint256 => bytes32) public tokenHashmap;
mapping(bytes32 => string) public hashToDomainMap;

uint256 public reset_period = 7257600; //12 weeks

bool public publicClaimOpen = false;
mapping(address => bool) public address_whitelist;

event TextChanged(bytes32 indexed node, string indexed indexedKey, string key);
event RegisterSubdomain(address indexed registrar, uint256 indexed token_id, string indexed label);

constructor(){
ens = ENS(0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e);
nft = IERC721Enumerable(0x9759226B2F8ddEFF81583e244Ef3bd13AAA7e4A1);
domainHash = getDomainHash();
}

//<interface-functions>
function supportsInterface(bytes4 interfaceID) public pure returns (bool) {
return interfaceID == 0x3b3b57de //addr
|| interfaceID == 0x59d1d43c //text
|| interfaceID == 0x691f3431 //name
|| interfaceID == 0x01ffc9a7; //supportsInterface << [inception]
}

function text(bytes32 node, string calldata key) external view returns (string memory) {
uint256 token_id = hashToIdMap[node];
require(token_id > 0 && tokenHashmap[token_id] != 0x0, "Invalid address");
if(keccak256(abi.encodePacked(key)) == keccak256("avatar")){
//eip155 string did not seem to work in any supported dapps during testing despite the returned string being properly
//formatted. So the toggle was added so that we can direct link the image using http:// if this still does not work on
//mainnet
return useEIP155 ? string(abi.encodePacked("eip155:1/erc721:", addressToString(address(nft)), "/", token_id.toString()))
: string(abi.encodePacked(nftImageBaseUri, token_id.toString(),".png"));
}
else{
return texts[node][key];
}
}

function addr(bytes32 nodeID) public view returns (address) {
uint256 token_id = hashToIdMap[nodeID];
require(token_id > 0 && tokenHashmap[token_id] != 0x0, "Invalid address");
return nft.ownerOf(token_id);
}

function name(bytes32 node) view public returns (string memory){
return (hashToIdMap[node] == 0)
? ""
: string(abi.encodePacked(hashToDomainMap[node], ".", domainLabel, ".eth"));
}
//</interface-functions>

//--------------------------------------------------------------------------------------------//

//<read-functions>
function domainMap(string calldata label) public view returns(bytes32){
bytes32 encoded_label = keccak256(abi.encodePacked(label));
bytes32 big_hash = keccak256(abi.encodePacked(domainHash, encoded_label));
return hashToIdMap[big_hash] > 0 ? big_hash : bytes32(0x0);
}

function getClaimableIdsForAddress(address addy) public view returns(uint256[] memory){
if(((address_whitelist[addy] || publicClaimOpen)
&& block.timestamp > nextRegisterTimestamp[addy])
|| owner() == addy){
return getAllIds(addy);
}
else{
return new uint256[](0);
}
}

function getAllIds(address addy) private view returns(uint256[] memory){
uint256 balance = nft.balanceOf(addy);
uint256[] memory ids = new uint256[](balance);
uint256 count;
for(uint256 i; i < balance; i++){
uint256 id = nft.tokenOfOwnerByIndex(addy, i);
if(tokenHashmap[id] == 0x0){
ids[count++] = id;
}
}

uint256[] memory trim_ids = new uint256[](count);
for(uint256 i; i < count; i++){
trim_ids[i] = ids[i];
}

return trim_ids;
}

function getTokenDomain(uint256 token_id) private view returns(string memory uri){
require(tokenHashmap[token_id] != 0x0, "Token does not have an ENS register");
uri = string(abi.encodePacked(hashToDomainMap[tokenHashmap[token_id]] ,"." ,domainLabel, ".eth"));
}

function getTokensDomains(uint256[] memory token_ids) public view returns(string[] memory){
string[] memory uris = new string[](token_ids.length);
for(uint256 i; i < token_ids.length; i++){
uris[i] = getTokenDomain(token_ids[i]);
}
return uris;
}

function getAllCatsWithDomains(address addy) public view returns(uint256[] memory){
uint256 balance = nft.balanceOf(addy);
uint256[] memory ids = new uint256[](balance);
uint256 count;
for(uint256 i; i < balance; i++){
uint256 id = nft.tokenOfOwnerByIndex(addy, i);
if(tokenHashmap[id] != 0x0){
ids[count++] = id;
}
}

uint256[] memory trim_ids = new uint256[](count);
for(uint256 i; i < count; i++){
trim_ids[i] = ids[i];
}

return trim_ids;
}
//</read-functions>

//--------------------------------------------------------------------------------------------//

//<helper-functions>
function addressToString(address _addr) private pure returns(string memory) {
bytes32 value = bytes32(uint256(uint160(_addr)));
bytes memory alphabet = "0123456789abcdef";

bytes memory str = new bytes(51);
str[0] = "0";
str[1] = "x";
for (uint i = 0; i < 20; i++) {
str[2+i*2] = alphabet[uint(uint8(value[i + 12] >> 4))];
str[3+i*2] = alphabet[uint(uint8(value[i + 12] & 0x0f))];
}
return string(str);
}

//this is the correct method for creating a 2 level ENS namehash
function getDomainHash() private view returns (bytes32 namehash) {
namehash = 0x0;
namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked('eth'))));
namehash = keccak256(abi.encodePacked(namehash, keccak256(abi.encodePacked(domainLabel))));
}
//</helper-functions>

//--------------------------------------------------------------------------------------------//

//<authorised-functions>
function setDomain(string calldata label, uint256 token_id) public isAuthorised(token_id) {
require(tokenHashmap[token_id] == 0x0, "Token has already been set");
require(address_whitelist[msg.sender] || publicClaimOpen || owner() == msg.sender, "Not authorised");
require(block.timestamp > nextRegisterTimestamp[msg.sender], "Wallet must wait more time to register");

bytes32 encoded_label = keccak256(abi.encodePacked(label));
bytes32 big_hash = keccak256(abi.encodePacked(domainHash, encoded_label));

//contract owner can update / overwrite records. << this may be changed in the future with an updated method but as this is still
//an experiment we'd like to retain some level of control over the sub-domains
//
//ens.recordExists seems to not be reliable (tested removing records through ENS control panel and this still returns true)
require(!ens.recordExists(big_hash) || msg.sender == owner(), "sub-domain already exists");

ens.setSubnodeRecord(domainHash, encoded_label, owner(), address(this), 0);

hashToIdMap[big_hash] = token_id;
tokenHashmap[token_id] = big_hash;
hashToDomainMap[big_hash] = label;

if (owner() != msg.sender){
nextRegisterTimestamp[msg.sender] = block.timestamp + reset_period;

//if user is on whitelist then remove
if (address_whitelist[msg.sender]){
address_whitelist[msg.sender] = false;
}
}

emit RegisterSubdomain(nft.ownerOf(token_id), token_id, label);
}

function setText(bytes32 node, string calldata key, string calldata value) external isAuthorised(hashToIdMap[node]) {
uint256 token_id = hashToIdMap[node];
require(token_id > 0 && tokenHashmap[token_id] != 0x0, "Invalid address");
require(keccak256(abi.encodePacked(key)) != keccak256("avatar"), "cannot set avatar");

texts[node][key] = value;
emit TextChanged(node, key, key);
}

function resetHash(uint256 token_id) public isAuthorised(token_id) {

bytes32 domain = tokenHashmap[token_id];
require(ens.recordExists(domain), "Sub-domain does not exist");

//reset domain mappings
hashToDomainMap[domain] = "";
hashToIdMap[domain] = 0;
tokenHashmap[token_id] = 0x0;

//allow sender to reclaim (if public == true)
if(nextRegisterTimestamp[msg.sender] > block.timestamp && msg.sender != owner()){
nextRegisterTimestamp[msg.sender] = block.timestamp + (60 * 30); //30 minute cooldown
}

}
//</authorised-functions>

//--------------------------------------------------------------------------------------------//

// <owner-functions>
function addAddressWhitelist(address[] calldata addresses) public onlyOwner {
for(uint256 i; i < addresses.length; i++){
address_whitelist[addresses[i]] = true;
}
}

function setDomainLabel(string calldata label) public onlyOwner {
domainLabel = label;
domainHash = getDomainHash();
}

function setNftAddress(address addy) public onlyOwner{
nft = IERC721Enumerable(addy);
}

function toggleNftImageLink() public onlyOwner{
useEIP155 = !useEIP155;
}

function setNftImageBaseUri(string memory _uri) public onlyOwner{
nftImageBaseUri = _uri;
}

function setEnsAddress(address addy) public onlyOwner {
ens = ENS(addy);
}

function resetAddressForClaim(address addy) public onlyOwner {
nextRegisterTimestamp[addy] = 0;
}

function togglePublicClaim() public onlyOwner {
publicClaimOpen = !publicClaimOpen;
}

function updateResetPeriod(uint256 time) public onlyOwner {
reset_period = time;
}

function renounceOwnership() public override onlyOwner {
require(false, "ENS is responsibility. You cannot renounce ownership.");
super.renounceOwnership();
}

//just never know.. do you.
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
payable(msg.sender).transfer(balance);
}

function withdrawTokens(IERC20 token) public onlyOwner {
require(address(token) != address(0));
uint256 balance = token.balanceOf(address(this));
token.transfer(msg.sender, balance);
}

//</owner-functions>

modifier isAuthorised(uint256 tokenId) {
require(owner() == msg.sender || nft.ownerOf(tokenId) == msg.sender, "Not authorised");
_;
}
}

Analysis

TBD, no plan yet.

You can also contribute to PCC Archive.