ENSコントラクトはどのように動いているのか?
今回はEthereum Name Service(ENS)のコントラクトで実際にどの様な処理が行われてるかを解説したいと思います。ENSについては、こちらで公開されているコントラクトを参照しました。
ENSの詳細についてはこちらの記事をご覧ください。
なお本記事ではアドレスを割り当てた.ethで終わる文字をETH名とします。
ENSコントラクトの構成要素
ENSはEIP137で基本機能が定義されている、ETH名とアドレスをマッピングするプロトコル提案です。EIP137自体はシンプルな提案ですが、今回取り上げるENSを実装したコントラクトはより複雑なものになっています。大きな構成要素としては
・名前とアドレスを紐付けるリゾルバ(ENSの基本機能)
・ERC721に準拠した実装
・上記2つを組み合わせたためにERC721のトークンIDと名前を紐づけるためのリゾルバ
といったものがあり、これらに焦点を当てて全体像と仕組みを解説していきます。
ENSコントラクトの全体像
ENSの全体像は下図のようになっています。
図では一部名称が見切れていますが、一番左下のETHRegistrarControllerコントラクトが実際のETH名の登録をする際に呼び出されるコントラクトです。実際のETH名の登録は、BaseRegistrarの中で定義された関数をETHRegistrarControllerから呼び出すことで行われます。この中で、ETH名のオーナーをセットしたり、そのETH名とアドレスの紐付けなどを行います。
BaseRegistrarはIERC721を引き継いでおり、トークンの保持数の取得等をこのコントラクトで行うこともできます。
ENSに登録するための関数
任意の名前をENSに登録する際には、ENSコントラクトのregisterWithConfigを通ります。
また、登録後にそのETH名を実際に使用可能にするにはリゾルバを設定する必要があります。リゾルバの設定にもこのregisterWithConfigを使用します。
registerWithConfigは以下の様に定義されています。
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)) {
expires = base.register(tokenId, address(this), duration);
bytes32 nodehash = keccak256(abi.encodePacked(base.baseNode(), label));
base.ens().setResolver(nodehash, resolver);
if (addr != address(0)) {
Resolver(resolver).setAddr(nodehash, addr);
}
base.transferFrom(address(this), owner, tokenId);
} else {
require(addr == address(0));
expires = base.register(tokenId, owner, duration);
}
emit NameRegistered(name, label, owner, cost, expires);
if(msg.value > cost) {
msg.sender.transfer(msg.value - cost);
}
}
下記はメソッド内で定義されている変数です。
・addr:ETH名をセットするアドレス
・commitment:ENSの登録申請、リゾルバ設定時それぞれのアクションにつける識別子
・cost:ETH名登録に必要なEther費用。
・label:ラベル。ENSで登録する任意の名前をkeccak256を介して16進数32バイトの長さに変換したもの
・tokenId:ENSトークンID。labelを10進数に変換したもの
・expires:登録したETH名の有効期限(期限が切れるタイムスタンプ )
ETH名の登録時にリゾルバを設定するかしないかで条件分岐があります。リゾルバとは、名前解決のために必要な機能です。例えば設定すると、ENS対応ウォレットからあるETH名に送金を行うことができます。下図のように裏ではウォレットがリゾルバにアクセスし、○○○.ethが0x….というようにETH名に紐づいたアドレスを取得します。
つまり、リゾルバを設定しない場合にはそのETH名のオーナー権限を持つだけなので、ETH名での送金等は行えません。リゾルバを設定し、更にそのリゾルバでETH名とアドレスの紐付けを行って初めてETH名での取引が可能になります。
リゾルバを設定しないENS登録の動き
対象のENSトークンIDに対して所有者(オーナー権限)を設定する、つまりオーナーアドレスを設定するだけです。これによりENSトークンIDを所有しているアドレスを登録できます。
関数の動きとしては、tokenId変数に対してオーナーアドレスを設定するだけであり、BaseRegisterからregisterメソッドを以下のように呼び出します。duration変数は登録期間で、登録したENSに対してオーナー権限を持てる期間を設定します。この期間は1年単位設定可能で、更新も可能です。ETH名の有効期限が切れるタイムスタンプを返します。
expires = base.register(tokenId, owner, duration);
リゾルバを設定するENS登録の動き
リゾルバを設定する場合は名前解決(任意のアドレスとETH名の紐付け)も同時に行うことができます。リゾルバを設定しない場合は単純にETH名の所有者を登録するだけでしたが、名前解決を行うことで実際にETH名が使用可能、つまりETH名での送金等が行えるようになります。
関数の動きとしては、まずオーナー権限を与えたいアドレスではなく、コントラクト自体に一旦オーナー権限を与えて、ETH名を登録します。(理由は後述します)
つまりトランザクション送信者が指定したオーナー権限を持たせたいアドレスではなく、トランザクションを送った先のコントラクト、つまりENSコントラクト自体が一時的にETH名のオーナー権限を持ちます。
expires = base.register(tokenId, address(this), duration);
登録したいETH名をkeccak256でハッシュ化してエンコードします。
bytes32 nodehash = keccak256(abi.encodePacked(base.baseNode(), label));
・keccak256:256ビットの16進数に変換するハッシュ関数
・base.baseNode():TLDのハッシュ値
・abi.encodePacked():渡された引数をabi方式にエンコードする
登録したENSに対してリゾルバを設定します。この際に外部コントラクトのメソッドを呼び出します。リゾルバを設定する呼び出し先のメソッドでは、呼び出し元のアドレスがETH名のオーナー権限を持っている必要あります。今回ENSコントラクトから呼び出すため、呼び出し元のアドレスはENSコントラクトになります。そのため、先ほどETH名を登録する際に一度、コントラクトをオーナーにしました。
base.ens().setResolver(nodehash, resolver);
そして、引数にETH名と紐付けたいアドレスを渡していれば、リゾルバに、アドレスとETH名の紐付けを登録します。
if (addr != address(0)) {
Resolver(resolver).setAddr(nodehash, addr);
}
そして、最後にETH名のオーナー権限をコントラクトから設定したいオーナーアドレスに移します。
base.transferFrom(address(this), owner, tokenId);
これがENS登録の概要です。
リゾルバの名前解決について
次にリゾルバが具体的にどの様に名前解決を行っているかを解説します。
現在、ENSでETH名を登録する際に、デフォルトではパブリックリゾルバを設定します。このリゾルバでは、アドレスの登録と参照を行うことができます。
ETH名からアドレスを参照するには、node(ETH名をENSコントラクト独自方式でエンコードしたもの)とcoinTypeが必要になります。以前の記事(リンク)でも解説していますが、ENSではEthereumのアドレス以外も登録可能であるため、ETH名に紐付けているコイン(例えばビットコイン)を指定する必要があります。
function addr(bytes32 node, uint coinType) public view returns(bytes memory) {
return _addresses[node][coinType];
}
そのため、先に示したリゾルバの図は正確には、下図のようになります。
また、参照するアドレスはEthererumアドレスとは限らないので、登録されているアドレスはアドレス型ではなく、バイト配列になっています。
同様に、アドレスを設定するには、ETH名、登録するアドレス、コインタイプが必要です。
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;
}
a:登録したいアドレス(バイト配列)
Ethereumのアドレスを登録する場合は、アドレスをバイト配列に変換する必要があります。
function setAddr(bytes32 node, address a) external authorised(node) {
setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
}
これがリゾルバによる名前解決の概要です。
まとめ
今回はENSコントラクトで任意のETH名を登録する流れを解説しました。
一口にETH名を登録すると言っても、リゾルバを設定する、しないの条件分岐があり、それによってENSコントラクト内での処理が変わります。
リゾルバを設定しない場合:
あるETH名の所有権を特定のアドレスに持たせるだけの処理となります。そのため、そのETH名での取引は行えません。例えばgoogle.ethは既にあるアドレスによって所有されていますが、リゾルバを設定していないためにmetamask等でgoogle.eth宛てに送金を行おうとしてもできません。
リゾルバを設定する場合:
ETH名とアドレスを紐付けるリゾルバを設定しますが、リゾルバを設定した後に、名前解決(ETH名とアドレスの紐付け)をしないと、これも同様にそのETH名での取引は行えません。つまり、ETH名での取引を行うには、名前解決を行うリゾルバの設定と名前解決の両方を行う必要があります。基本的にはリゾルバを設定する際に、同時に名前解決も行います。
ENSではEthereumアドレス以外のアドレス(ビットコインやリップル)を登録することもできるので、名前解決する際にはどのコインタイプを登録するかも指定する必要があります。マルチコイン対応のウォレット(xxx.ethという名前でEthereumアドレス以外のアドレスを参照できるウォレット)は上記のように登録されたETH名を参照することで、例えばビットコインアドレスとgoogle.ethのようなETH名を紐づけています(※記事執筆時にはgoogle.ethとビットコインアドレスは紐づいていません)。
(ところで)
ENS公式ブログにて報告がありましたが、最近ENSコントラクトの脆弱性が発見されました(修正済みのコントラクトがデプロイ済み)。
バグについての詳細については記述がありませんでしが、一度保有したことあるETH名に何かしこむと、保有権を持ってなくても勝手に売ったり転送したりできるバグだそうです。このバグは今のところ悪用されていないとのことです。
ユーザー側にはアクションは求められていませんが、古いコントラクトを使ったサービスを提供している場合は新しいコントラクトへの以降を推奨しています。
エンジニアがブロックチェーンを見るマガジンでは、単純なプロジェクトやプロダクトの紹介だけでなく、その裏で何が起きているかを技術視点、ビジネス視点から紹介していこうと思いますので、是非フォローお願いします!
この記事が気に入ったらサポートをしてみませんか?