ENS注册流程解析

  • frank
  • 更新于 2022-07-23 11:43
  • 阅读 3202

ENS的注册分为两步,先commit预提交,再registerWithConfig注册。

1. 注册

ENS的注册分为两步,先commit预提交,再registerWithConfig注册。

先看下commit的代码,客户端会先调用makeCommitmentWithConfig获得commitment参数,再调用commit进行预提交。

//存储预提交记录的时间戳
mapping(bytes32=>uint) public commitments;
//最小预提交间隔时间,单位:秒
uint public minCommitmentAge;
//最大预提交间隔时间,单位:秒
uint public maxCommitmentAge;
/**
* @dev 生成commitment参数
* @param name ens名
* @param owner 注册者
* @param secret 32随机字节
* @param resolver 正向解析器地睛
* @param addr 正向解析目标地址
*/
function makeCommitmentWithConfig(string memory name, address owner, bytes32 secret, address resolver, address addr) pure public returns(bytes32) {
  bytes32 label = keccak256(bytes(name));
  if (resolver == address(0) && addr == address(0)) {
    return keccak256(abi.encodePacked(label, owner, secret));
  }
  require(resolver != address(0));
  return keccak256(abi.encodePacked(label, owner, resolver, addr, secret));
}
/**
* @dev 预提交
* @param commitment 提交参数
*/
function commit(bytes32 commitment) public {
  require(commitments[commitment] + maxCommitmentAge < block.timestamp);//当重复预提交时,如果时间还未超过最大时间间隔,就不用再重新更新时间戳
  commitments[commitment] = block.timestamp;//记录本此提交的时间戳
}
function randomSecret() {
  return '0x' + crypto.randomBytes(32).toString('hex')
}//生成makeCommitmentWithConfig的secret参数,实际是32随机字节

此步预提交的目的是为了防止抢跑,如果没有这一步的话,直接一步就是注册域名成功,那么在mev里可以监听注册域名的tx,然后用gasPrice去抢跑,那么一些抢手的域名注册时就可能会被机器人抢走。加上这个预提交步骤后,机器人如果要抢跑域名注册也要监听commit方法,但这个方法的参数的bytes32,解读不出tx是要注册哪个域名,机器人自然就抢不了;而如果机器人直接监听第二步registerWithConfig,但没有先进行第一步预提交tx,自然也成功不了。

下面来看下registerWithConfig方法

contract ETHRegistrarController is Ownable {
    BaseRegistrarImplementation base;
    PriceOracle prices;
    uint public minCommitmentAge;
    uint public maxCommitmentAge;
    mapping(bytes32=>uint) public commitments;  
    /**
    * @dev 注册ens并配置
    * @param name ens名
    * @param owner 注册者
    * @param duration 域名有效时间
    * @param secret 32随机字节
    * @param resolver 正向解析器地睛
    * @param addr 正向解析目标地址
    */
    function registerWithConfig(string memory name, address owner, uint duration, bytes32 secret, address resolver, address addr) public payable {
        bytes32 commitment = makeCommitmentWithConfig(name, owner, secret, resolver, addr);
        uint cost = _consumeCommitment(name, duration, commitment);

        bytes32 label = keccak256(bytes(name));
        uint256 tokenId = uint256(label);

        uint expires;
        //存在正向解析器
        if(resolver != address(0)) {
            //mint NFT,设置ens的注册者为address(this),这里先临街把注册者设为当前合约地址,
            //因为后面要setResolver时,要求msg.sender是合约注册者,如果直接把注册者给到owner,那在setResolver时就会fail
            expires = base.register(tokenId, address(this), duration);

            //计算nodehash
            bytes32 nodehash = keccak256(abi.encodePacked(base.baseNode(), label));

            //设置正向解析器地址
            base.ens().setResolver(nodehash, resolver);

            //如果正向解析地址不为0,就设置正向解析地址
            if (addr != address(0)) {
                Resolver(resolver).setAddr(nodehash, addr);
            }

            //用owner重新认领这个ens
            base.reclaim(tokenId, owner);
            //转移erc721 NFT给owner
            base.transferFrom(address(this), owner, tokenId);
        } else {//不存在正向解析器,直接把注册者设为owner就可以了
            require(addr == address(0));
            expires = base.register(tokenId, owner, duration);
        }
        //emit注册事件
        emit NameRegistered(name, label, owner, cost, expires);

        //发送过来的eth超过了费用,就退还
        if(msg.value > cost) {
            payable(msg.sender).transfer(msg.value - cost);
        }
    }
    /**
    * @dev 消费commitment
    * @param name ens名
    * @param duration 域名有效时间  
    * @param commitment 参数 
    */
    function _consumeCommitment(string memory name, uint duration, bytes32 commitment) internal returns (uint256) {
        //commit之后必须等待最小间隔时间后才能注册,当前是60秒
        require(commitments[commitment] + minCommitmentAge <= block.timestamp);
        //不能超过commit之后的最大间隔境,当前是7天
        require(commitments[commitment] + maxCommitmentAge > block.timestamp);
        require(available(name));//这个ens要可用
        delete(commitments[commitment]);//删除预提交信息,gas返还
        uint cost = rentPrice(name, duration);//根据域名购买的有效时间,计算费用
        require(duration >= MIN_REGISTRATION_DURATION);//有效时间不能小于最小时间,当前是28天
        require(msg.value >= cost);//传入的eth要>=费用
        return cost;
    }
    /**
    * @dev 价格计算
    * @param name ens名
    * @param duration 域名有效时间  
    */
    function rentPrice(string memory name, uint duration) view public returns(uint) {
        bytes32 hash = keccak256(bytes(name));
        return prices.price(name, base.nameExpires(uint256(hash)), duration);
    }
}
contract StablePriceOracle is Ownable, PriceOracle {    
    //索引2,3,4分别存储域名3,4,5位及以上的每秒的usd费用
    //现在是3位每年640美金,4位每年160美金,5位及以上每年5美金
    uint[] public rentPrices;
    function price(string calldata name, uint expires, uint duration) external view override returns(uint) {
        uint len = name.strlen();
        if(len > rentPrices.length) {
            len = rentPrices.length;
        }
        require(len > 0);

        uint basePrice = rentPrices[len - 1].mul(duration);//每秒费用*有效时间
        basePrice = basePrice.add(_premium(name, expires, duration));//这里_premium固定是0

        return attoUSDToWei(basePrice);//根据预言机转成wei价格,用的是MakerDAO的medianizer预言机
    }
}
contract BaseRegistrarImplementation is ERC721, BaseRegistrar  {
    // A map of expiry times
    mapping(uint256=>uint) expiries;
    modifier live {
        require(ens.owner(baseNode) == address(this));//基结点的所有者必须是本合约
        _;
    }

    modifier onlyController {
        require(controllers[msg.sender]);//调用方地址必须是可控制这个合约的地址,这个地址需要本合约owner事先用addController添加进来
        _;
    }
    //返回域名是否可以注册
    function available(uint256 id) public view override returns(bool) {        
        return expiries[id] + GRACE_PERIOD < block.timestamp;//过期时间戳+90天的保护期<当前时间戳,就表示过期且过了保护期了,可以注册
    }
    function _register(uint256 id, address owner, uint duration, bool updateRegistry) internal live onlyController returns(uint) {
        require(available(id));
        require(block.timestamp + duration + GRACE_PERIOD > block.timestamp + GRACE_PERIOD); //先计算一下当前时间戳+有效时间+保护期,如果溢出就会直接fail,防止这次注册成功后,后面都不可renew了

        expiries[id] = block.timestamp + duration;
        if(_exists(id)) {
            //之前存在这个id的nft,则燃烧,说明这个nft是过期又注册的
            _burn(id);
        }
        _mint(owner, id);
        if(updateRegistry) {//更新ens的owner
            ens.setSubnodeOwner(baseNode, bytes32(id), owner);
        }

        emit NameRegistered(id, owner, block.timestamp + duration);

        return block.timestamp + duration;
    }
    /**
     * @dev 重新认领ens
     */
    function reclaim(uint256 id, address owner) external override live {
        require(_isApprovedOrOwner(msg.sender, id));
        ens.setSubnodeOwner(baseNode, bytes32(id), owner);
    }
}

讲到这里,需要说明一下nodehash的生成过程,以btc.eth为例,

keccak256(“btc”)得到0x4bac7d8baf3f4f429951de9baff555c2f70564c6a43361e09971ef219908703d,再keccak256(abi.encodePacked(base.baseNode(), “0x4bac7d8baf3f4f429951de9baff555c2f70564c6a43361e09971ef219908703d”))得到0x9b530388C920f6b1dD3d05AEFb9B4650Fe388B2F,就是实际btc.eth在ENS系统中的node,而baseNode=keccak256(abi.encodePacked(“0x00000000000000000000000000000000”, keccak256(“eth”))。如果是btc.eth的子域名就再递归下去。

contract ENSRegistry is ENS {
    //解析器结构体
    struct Record {
        address owner;//注册者
        address resolver;//解析器合约地址
        uint64 ttl;
    }
    /**
     * @dev Sets the resolver address for the specified node.
     * @param node The node to update.
     * @param resolver The address of the resolver.
     */
    function setResolver(bytes32 node, address resolver) public virtual override authorised(node) {
        emit NewResolver(node, resolver);
        records[node].resolver = resolver;//设置node的解析器地址,这里的node就是上面讲到的nodehash生成的node
    }
}
abstract contract AddrResolver is ResolverBase {    
    uint constant private COIN_TYPE_ETH = 60;//eth地址默认60,还可以解析btc,ltc,dego地址,分别对应0,2,3
    mapping(bytes32=>mapping(uint=>bytes)) _addresses;//此map存储node=>60=>address,就表示某个eth域名对应的地址
    /**
     * Sets the address associated with an ENS node.
     * May only be called by the owner of that node in the ENS registry.
     * @param node The node to update.
     * @param a The address to set.
     */
    function setAddr(bytes32 node, address a) external authorised(node) {
        setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
    }   

    function setAddr(bytes32 node, uint coinType, bytes memory a) public authorised(node) {
        emit AddressChanged(node, coinType, a);
        if(coinType == COIN_TYPE_ETH) {
            emit AddrChanged(node, bytesToAddress(a));
        }
        _addresses[node][coinType] = a;
    }   
}

ENS域名也是ERC721的NFT,可以以opensea等平台进行交易,但是它没有metadata,且在交易成功后,新的owner需要调用一下BaseRegistrarImplementation合约的reclaim方法,重新认领一下,才可以管理自己的ENS域名。

在域名过期后,设有90天的保护期,在保护期内owner是可以直接renew的,也就是续期,过了保护期后就要重新commit去注册了,这时候其他人也就可以注册这个域名了。

2. 反向解析

上面讲的是用ens域名解析出用户的钱包地址,下面来讲下怎么反向解析,也就是用钱包地址反向解析出ens域名。

contract ReverseRegistrar {
    // namehash('addr.reverse')
    bytes32 public constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;

    function claimWithResolver(address owner, address resolver) public returns (bytes32) {
        bytes32 label = sha3HexAddress(msg.sender);//将钱包地址转成字符串再做keccak256
        bytes32 node = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, label));
        address currentOwner = ens.owner(node);

        //需要更新解析器地址
        if (resolver != address(0x0) && resolver != ens.resolver(node)) {
            // Transfer the name to us first if it's not already
            if (currentOwner != address(this)) {//之前的owner不是当前合约地址
                ens.setSubnodeOwner(ADDR_REVERSE_NODE, label, address(this));//设置node的owner为当前合约地址
                currentOwner = address(this);
            }
            ens.setResolver(node, resolver);//设置解析器地址
        }

        //如果用了不同的ReverseRegistar时,就会出现之前的owner和不是当前合约地址,需要更新
        if (currentOwner != owner) {
            ens.setSubnodeOwner(ADDR_REVERSE_NODE, label, owner);
        }

        return node;
    }

    function setName(string memory name) public returns (bytes32) {
        bytes32 node = claimWithResolver(address(this), address(defaultResolver));
        defaultResolver.setName(node, name);//设置node对应的ens name
        return node;
    }    
}
contract DefaultReverseResolver {    
    mapping (bytes32 => string) public name;    
    function setName(bytes32 node, string memory _name) public onlyOwner(node) {
        name[node] = _name;
    }
}

从代码出可以看出,任何地址都可以设置反向解析到指定ens域名,没有做ens域名的所有者限制。与正向解析用eth做根不同的时,反向解析用addr.reverse做根。

3. 管理权限

ENS的owner可以将ens授权给其它地址, 这些地址可以理解为管理地址,可以对这个域名设置各自解析器等操作,但不能转让这个nft。

contract ENSRegistry is ENS {
   function setApprovalForAll(address operator, bool approved) external virtual override {
        operators[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }
}

可以看到BaseRegistrarImplementation继承了ERC721,最终还是用了erc721的owner体系去实现nft的归属,在ENSRegistry里维护的是ens业务上的所有者、解析器等信息。因此,如果在opensea上交易后,只完成的nft的转让,还需要再reclaim一次,才能自己设置地址解析。

contract BaseRegistrarImplementation is ERC721, BaseRegistrar  {
    /**
     * @dev 重新认领ens
     */
    function reclaim(uint256 id, address owner) external override live {
        require(_isApprovedOrOwner(msg.sender, id));
        ens.setSubnodeOwner(baseNode, bytes32(id), owner);
    }
}

4. 小技巧

ENS有提案允许保存ABI信息到链上,并规定了几种contentType,如下:

1 JSON 2 zlib-compressed JSON 4 CBOR 8 URI 要求contentType是2的指数,在代码里用了位运算来实现这个判断。

例如:contentType=8,二进制是1000,将contentType-1=7,二进制是0111,将0111与1000做与运算,得到0,也就是说-1后再&运算将永远是0,以此来判断2的指数。

function setABI(bytes32 node, uint256 contentType, bytes calldata data) external authorised(node) {
  // Content types must be powers of 2
  require(((contentType - 1) & contentType) == 0);

  abis[node][contentType] = data;
  emit ABIChanged(node, contentType);
}
点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
frank
frank
江湖只有他的大名,没有他的介绍。