フルオンチェーンNFTの新時代を切り拓くか|dom氏が作成した「ROSES」について徹底解説

こんにちは、フルオンチェーンNFTクリエイターのnawooです。

先日、dom氏(@dhof)が Twitter上で、新作NFTとなる「ROSES」を発表しました。

上のGIFアニメーションを確認していただくと『3Dのバラが回転している』ことが分かりますが、なんと驚くべきことにこちらはEthereumでの「フルオンチェーンNFT」となっています!

しかも、トークンごとに「バラの形」「色」が異なる『ジェネラティブアート』になっているのです。

出典:Rose 1, Rose 7, Rose 29

では一体、dom氏はどうやってフルオンチェーンで3Dを実現しているのか。

nawoo

その謎に迫るべく筆者はこの度、本NFTの元となるコントラクトコードを読んでみました。

ということで本記事では、先日dom氏が作成したフルオンチェーンNFT「ROSES」についてご紹介することで、本プロジェクトの概要ならびにコントラクトの中身、フルオンチェーンNFTの新時代を切り拓く可能性などを理解していただくことを目的とします。

でははじめに、この記事の構成について説明します。

STEP
前書き|「ROSES」とは

まずは、本記事の題材となっている『ROSES』について、執筆時点で公開されている情報をもとに概要を述べてまいります。

STEP
「ROSES」がフルオンチェーンNFTであることを確認

続いて、tokenURI関数ならびに実際のHTMLの内容を確認していくことで、「ROSES」がフルオンチェーンで3Dを実現していることを確認してまいります。

STEP
関連するコントラクトについて解説

最後に、STEP2で解説した「複雑なHTML」をどうやってコントラクトで作成しているかについて、解説してまいります。

本記事が、「ROSES」の概要や注目ポイント、斬新なフルオンチェーンNFTの作成方法などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。

※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。

【AD】Nouns DAO JAPAN

Nouns DAO JAPANは世界で一番Nounsを広げるコミュニティを目指します。Discord参加はこちら

目次

前書き|「ROSES」とは

「ROSES」作成者のdom氏の発表によると、執筆時点においては『技術的な詳細は近日中に公開予定』となっており、現時点でNFTに関する詳細は未公表といった状況です。

それを踏まえて、本記事ではEtherscanで公開されているスマートコントラクトの情報をもとに、本NFTプロジェクトがどのように「3D×フルオンチェーン」を実現しているのかなどについて、私見を含めて解説してまいります。

出典:OpenSea

なお、ROSESに関して現時点で分かっている情報は以下の通りです。

  • Ethereumチェーン上の3Dアニメーション×フルオンチェーンNFT
  • NFTの最大発行数は1024個
  • 執筆時点ではmint不可能な状態であり、なおかつ詳細も不明
  • tokenId=1のNFTは、コントラクトのデプロイ時にdom氏によってあらかじめmintされている
  • さらに現在発行されているtokenId=2〜32のNFTは、reserve関数からmintされている
  • reserve関数は、dom氏が自分用に31枚をmintするための関数の模様

次章からは、このROSESがどのようにEthereumチェーン上で、「フルオンチェーン×3D」を実現しているのかについて、確認していきましょう。

「ROSES」がフルオンチェーンNFTであることを確認

続いて本章では、tokenURI関数ならびに実際のHTMLの内容を確認していくことで、本記事の題材となっている『ROSES』がフルオンチェーンで3Dを実現していることを確認してまいります。

tokenURIについて

フルオンチェーンNFTでは、tokenURI 関数でdataURL化したメタデータを返します。

一般的には、メタデータのimageにdataURL化したSVGファイルを指定することが多いです。

例えば、Mt.Chickenのメタデータは以下のようになっています。

{
  "name": "MtChicken #1",
  "description": "Mt. Chicken is a NFT ...",
  "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0..."
}

 name,description,imageの3つが設定されていて、imageにはSVGファイルが指定されていることが分かります。

それでは、ROSESのtokenURI関数を実行してみましょう。 (こちらはEtherscanから実行できます)

出典: https://etherscan.io/address/0x3e743377417cd7ca70dcc9bf08fac55664ed3181#readContract

フルオンチェーンNFTなので、dataURL形式で書かれたメタデータ(JSON)が返ってきますが、上写真の通り非常に長い文字列になっていますね。

nawoo

ちなみにこちらをDLしてみると、なんと271KBもありました!

続いて、dataURL(先ほどの非常に長い文字列)をデコードして整形してみると、メタデータは以下のようになっていました。

{
  "animation_url": "data:text/html,%3Cscript%20src%3D%27data%3Atext%2Fjavascript ~ t%3E",
  "name": "Rose 1",
}

ご覧の通り、ROSESのメタデータではimageではなく、animation_urlが設定されていることがお分かりでしょう。

さらに、dataURLのmediaTypeはtext/htmlになっているので、SVGではなくHTMLが書かれていることが分かります。

OpenSeaは、animation_url でHTMLを指定したNFTにも対応しているので、dataURLでHTMLを返すことによって『SVGではなくHTMLを使ったフルオンチェーンNFT』が実現できるというカラクリです。

そして、単なるHTMLだけでなく、JavaScirptを使ったHTMLにも対応しています。

出典: https://docs.opensea.io/docs/metadata-standards

以上、まずはtokenURIを確認してみたことで、ROSESでは以下の方法をとっていることが分かりました。

  • three.jsという、3D描画用のJavaScriptライブラリを使っている
  • 3Dのバラを作成するJavaScriptコードを作成し、HTMLに埋め込んでいる
  • HTMLをdataURL化して、animation_urlで返している

HTMLの内容について

続いて本節では、実際のHTMLの内容を確認していきます。

下記は、animation_urlをデコードして整形したものですが、5つの<script>と1つの<style>が出てきます。

nawoo

<!-- script 1 -->などのコメントは、筆者が追加したものです。

<!-- script 1 -->
<script src="data:text/javascript;base64,Y29uc3QgYmFzZT (中略) Cn0="></script>

<!-- script 2 -->
<script src="data:text/javascript;base64,IWZ1bmN0aW9uKG (中略) fSk="></script>

<!-- script 3 -->
<script>
  var data = base64ToBytes("H4sIAK/YHGMAA+x9eXfbOLLv/+9 (中略) 3AkA");
  var unzipped = fflate.gunzipSync(data);
  var text = fflate.strFromU8(unzipped);
  var b64 = btoa(text);
  var script = document.createElement("script");
  script.setAttribute("src", "data:text/javascript;base64," + b64);
  document.head.appendChild(script);
</script>

<!-- script 4 -->
<script>
  var tokenId = 1;
</script>

<!-- style -->
<style>
  * {
    margin: 0;
    padding: 0;
  }
  canvas {
    width: 100%;
    height: 100%;
  }
</style>

<!-- script 5 -->
<script>
  window.onload = () => {
    const o = (o) => (
      void 0 !== o && (l = o % 2147483647) <= 0 && (l += 2147483646),
      ((l = (16807 * l) % 2147483647) - 1) / 2147483646
    );
    o(tokenId);
    const t = 2 * Math.PI,
      i = window.innerWidth,
      n = window.innerHeight;
    // 中略
  };
</script>

各パートを簡単に説明すると、次のようになっています。

場所内容
script 1Base64を扱うためのJavaScriptライブラリ「base64.js」を、dataURL化したもの
script 2gzipを扱うためのJavaScriptライブラリ「fflate」を、dataURL化したもの
script 3three.js本体、およびthree.jsを読み込むためのscriptタグを生成するJavaScriptコード
script 4tokenId変数を定義するJavaScriptコード
styleスタイルシートの設定
script 5three.jsを使って3Dのバラを作成するJavaScriptコード
nawoo

次節では、この複雑なHTMLをどうやってコントラクトで作成しているかについて、一緒に見ていきましょう。

関連するコントラクトについて解説

ROSESのコントラクトは「Roses.sol」ですが、そこから呼び出されるコントラクトが多数あるので、以下の画像ならびにテーブルを用いて整理しておきます。

コントラクトアドレス内容
Roses.sol0x3e743377417cd7ca70dcc9bf08fac55664ed3181本体
HelloWorldsRenderer.sol0x168219161C8F88DE76027173227a569544FF03c1tokenURIを作成する
DataChunkCompiler.sol0xeC8EF4c339508224E063e43e30E2dCBe19D9c087HTMLのパーツを作成する
FFlateDataChunk1.sol0xa942F946A35545F50792DA1Ea1ADf0c3b619b921データコントラクト (fflate 1 of 2)
FFlateDataChunk2.sol0xF10EeDb5ACE715d78e0f89eCd1Dfc3E5874f6e3cデータコントラクト (fflate 2 of 2)
ThreeDataChunk1.sol0xA32bb79b33B29e483d0949C99EC0C439b29e2B33データコントラクト (three.js 1 of 9)
ThreeDataChunk2.sol0x0d104Dea962b090bC46c67a12e800ff16eeffB75データコントラクト (three.js 2 of 9)
ThreeDataChunk3.sol0x1D11a1c75e439A50734AEF3469aed9ca4fFe39fcデータコントラクト (three.js 3 of 9)
ThreeDataChunk4.sol0x6bAb43D4F3587f9f3ca1152C63E52BF7F8de2Dc1データコントラクト (three.js 4 of 9)
ThreeDataChunk5.sol0x57beAe62670Ff6cCf8311411a2A2aAb453413987データコントラクト (three.js 5 of 9)
ThreeDataChunk6.sol0xF3A95B30E1Fc2EdCea41fF93270249b6Ab979730データコントラクト (three.js 6 of 9)
ThreeDataChunk7.sol0x52a31D845f4bdC1D47Ee21dB7C25Bde2423A91Aeデータコントラクト (three.js 7 of 9)
ThreeDataChunk8.sol0x6CcCc7eA426E14F1E07528296c7d226677fd2fF6データコントラクト (three.js 8 of 9)
ThreeDataChunk9.sol0xc230862406bBe44f499943Ae4E9E6317a95BC7Adデータコントラクト (three.js 9 of 9)

本章では、それぞれの関連するコントラクの内容について、末端のものから順に解説してまいります。

データコントラクトについて

ThreeDataChunk 1〜9と FFlateDataChunk 1〜2は、『データを保存するためだけのコントラクト』です。

例えば、ThreeDataChunk1コントラクトの中身は以下だけとなっています。

contract ThreeDataChunk1 {
    string public constant data = 'H4sIAK/YHGMAA+x9eXfbOLLv (中略) f3gr3';
}

ここではdataが定義されていますが、 constant(定数) であることが重要です。

これによって、data はストレージではなく、コントラクトコードとして保存されています。

ストレージと異なりデプロイ時に書き込まれるため、デプロイ後の変更は不可となりますが、その代わりにread/writeにかかるガス代が非常に安くなります。

ちなみに、SSTORE2というライブラリを使うと、データコントラクトを簡単に扱うことができます。

話を本題に戻しますと、ThreeDataChunk1〜ThreeDataChunk9のdataには、three.jsのコードが分割されて入っています。

nawoo

厳密には、three.js のコードを gzip で圧縮して、Base64 エンコードしてから、9 つに分割したものが入っています。ややこしい(^^;

また、「FFlateDataChunk1」「FFlateDataChunk2」には、fflateのコードをdataURL化したものが分割されて入っていますが、こちらはサイズがそれほど大きくないためなのかgzip圧縮はされておりません。

「DataChunkCompiler.sol」について

DataChunkCompiler.solは、『JavaScriptやHTMLのパーツなどを作成しているコントラクト』です。

ROSESでは、「base64.js」「fflate」「three.js」という3種のJavaScriptライブラリを利用していますが、本コントラクトではこれらのJavaScriptライブラリを読み出して復元しています。

base64.js

出典:https://etherscan.io/address/0xeC8EF4c339508224E063e43e30E2dCBe19D9c087#code#F1#L49

こちらは、JavaScriptコードをdataURL化したものになります。

コントラクト内に定数として直接書かれていますが、こちらはbase64.jsのサイズが小さいため、データコントラクトにしなかったのではないかと考えられます。

string public constant base64Utils = 'data%253Atext%252Fjavascript%253Bbase64%2 ~ Cn0%253D';

この定数は、<script src="〜">の中に埋め込まれて、HTMLのscript 1の部分になります。

fflate

出典:https://etherscan.io/address/0xeC8EF4c339508224E063e43e30E2dCBe19D9c087#code#F1#L6

データコントラクトの FFlateDataChunk1,2 から読み込んでいます。

データコントラクトのdataは、IDataChunk(データコントラクトのアドレス).data() で読み出すことができます。

// DataChunkCompiler.sol
interface IDataChunk {
    function data() external view returns (string memory);
}
// 読み出しの例 (chunk1とchunk2のaddressからdataを読み出して結合して返す関数)
function compile2(address chunk1, address chunk2) public view returns (string memory) {
    IDataChunk data1 = IDataChunk(chunk1);
    IDataChunk data2 = IDataChunk(chunk2);
    return string(abi.encodePacked(data1.data(), data2.data()));
}

読み込んだデータは、<script src="〜">に埋め込まれて、HTMLのscript 2の部分になります。

three.js

さて、本命のthree.jsです。

three.jsのコードはminifyされていてもサイズが646KBもあり、fflateのように、<script src="〜">に直接埋め込むことができないようです。

nawoo

おそらく、文字列結合するとガス制約をオーバーしてしまうと思われます。

まず、gzip圧縮してBase64エンコードすると、218KBまで圧縮できます。

1つのデータコントラクトの上限は約24.5KBなので、データコントラクト9つに分割して保存しています。

データコントラクトからdataを読み出して結合し、gzip圧縮 & Base64エンコードされたままの状態で、JavaScriptの変数に代入しておきます。

あとはJavaScriptによって、gzip展開と<script src="〜">への埋め込みをおこなっています。

nawoo

JavaScriptで実行されるため、ガス制約が関係なくなります。

three.jsを読み込むための手順は、以下になります。

  1. あらかじめthree.jsをgzip圧縮して、base64エンコードしておく
  2. 9つに分割して、ThreeDataChunk1〜9としてデプロイする
  3. DataChunkCompilerでThreeDataChunk1〜9から読み出して結合し、threejs変数に代入する
  4. script 3を作成する (BEGIN_SCRIPT_DATA_COMPRESSED + threejs + END_SCRIPT_DATA_COMPRESSED)
  5. Base64デコードして、gzipを展開する
  6. 再びBase64エンコードをおこなってから、dataURL化する
  7. createElement で<script>タグを生成して、documentに追加する
  8. 生成した <script src="〜"> に、dataURLを設定する
nawoo

1,2 については、dom氏のPCでおこなったであろう作業になります
(公開されていないため推測です)。

3,4 は、コントラクトでおこなう処理です。

ThreeDataChunk1〜ThreeDataChunk9のdataを結合し、script 3 の base64ToBytes関数の引数として埋め込みます。

5-8 は、HTML内のscript 3でおこなわれる処理です。

Base64デコードをおこない、unzipして、再度Base64エンコードし、createElementで生成した<script src="〜">にdataURLとして設定するという内容になり、この処理はJavaScript(つまりブラウザ)で実行されます。

<!-- script 3 -->
<script>
  var data = base64ToBytes("H4sIAK/YHGMAA+x9eXfbOLLv/+9 (中略) 3AkA");
  var unzipped = fflate.gunzipSync(data);
  var text = fflate.strFromU8(unzipped);
  var b64 = btoa(text);
  var script = document.createElement("script");
  script.setAttribute("src", "data:text/javascript;base64," + b64);
  document.head.appendChild(script);
</script>
nawoo

ややこしいので図を作成してみました。

nawoo

しかし、よくこんな方法を思いつきますよね。
「すごい」としか言いようがないです。

「HelloWorldsRenderer.sol」について

nawoo

ここまでくればあと一息です。

本コントラクトの tokenURI関数で、DataChunkCompilerの変数や関数を呼び出し、それらを連結してHTMLとメタデータを作成しています。

「DataChunkCompiler」や「ThreeDataChunk1〜9」のアドレスは、setCompilerAddress関数とsetThreeAddress関数で設定しているので、トランザクションを見ればそれぞれのアドレスが分かります。

function setCompilerAddress(address newAddress) public {
    require(msg.sender == owner);
    compiler = IDataChunkCompiler(newAddress);
}

function setThreeAddress(address chunk1, address chunk2, address chunk3, address chunk4,
    address chunk5, address chunk6, address chunk7, address chunk8, address chunk9) public {
    require(msg.sender == owner);
    threeAddresses[0] = chunk1;
    (中略)
    threeAddresses[8] = chunk9;
}

3Dのバラを生成するJavaScriptコード(script 5)は、tokenURI 関数の中に直接書き込まれており、tokenIdという変数をシードにして、トークンごとに一つ一つ異なる3Dのバラを作成しています。

ただし、このJavaScriptコードはminifyされており、コードが読みづらくなっています。

nawoo

私はthree.jsにはあまり詳しくないので、よく分からなかったのですが、解読できた方がいらっしゃればぜひ教えて下さい。

<script>window.onload=(()=>{const o=o=>(void 0!==o&&(l=o%2147483647)<=0&&(l+=2147483646),((l=16807*l%2147483647)-1)/2147483646);o(tokenId);const t=2*Math.PI,i=window.innerWidth,n=window.innerHeight; (中略) for(var E of a.children){if(-1!=w.indexOf(E))return;var n=E.position.y+500;n/=1e4,E.position.y+=10*t*n,E.rotation.y+=.1*n}}()}()});</script>

今後、dom氏がGitHubなどを通して、minifyされる前のコードを公開してくれることを期待しましょう。

「Roses.sol」について

さて、最後のRosesコントラクトは、ERC721本体です。

uint256 constant maxSupply = 1024;

constructor() ERC721("Roses", "ROSE") Ownable() {
    // genesis mint
    _mint(msg.sender, 1);
    nextTokenID = 2;
}

記事冒頭で述べた通り、ROSESというNFTコレクションの最大発行数はmaxSupply=1024 となっており、 またtokenId=1はconstructorでmint(コントラクトのデプロイ時にdom氏によってあらかじめmint)されていることが分かります。

function reserve() public onlyOwner {
    // reserve 31 more
    uint256 start = nextTokenID;
    uint256 end = nextTokenID + 31;
    for (uint i = start; i < end; ++i) {
        _mint(msg.sender, i);
    }
    nextTokenID += 31;
}

reserve関数は、dom氏が自分用に31枚をmintするための関数のようであり、現在発行されているtokenId=2〜32のNFTは、reserve関数からmintされています。

function setAllowed(address addr, bool allowed) public onlyOwner {
    allowList[addr] = allowed;
}

function mint(address destination) public nonReentrant {
    require(nextTokenID <= maxSupply, 'complete');
    require(allowList[msg.sender] == true || msg.sender == owner(), 'not allowed or owner');
    _mint(destination, nextTokenID);
    nextTokenID++;
}

また、mintできるのはowner もしくはallowListに入っている人だけのようです。

function setRenderer(address addr) public onlyOwner {
    renderer = IHelloWorldsRenderer(addr);
}

function tokenURI(uint256 tokenID) override public view returns (string memory) {
    return renderer.tokenURI(tokenID);
}

setRenderer関数でレンダラーを設定し、tokenURI関数ではレンダラーの tokenURI関数を呼び出しているだけです。


以上が、ROSESに関連するコントラクトについての解説パートとなります。

まとめ

今回は、先日dom氏が作成したフルオンチェーンNFT「ROSES」について紹介・解説しました。

本記事が、「ROSES」の概要や注目ポイント、フルオンチェーンNFTの新時代を切り拓く可能性などについて理解したいと思われている方にとって、少しでもお役に立ったのであれば幸いです。

また励みになりますので、参考になったという方はぜひTwitterでのシェア・コメントなどしていただけると嬉しいです。

nawoo

さて、ざっと見ていきましたが、いかがでしたか?
Lootのコントラクトと比べると、かなり複雑になっていますね。

「フルオンチェーンNFTでHTMLを使う」というアイデアは他でも見たことがありましたが、まさかthree.jsを動かしてしまうとは、本当に驚きました。

サイズが大きいファイルをgzip圧縮しておいて、JavaScriptで展開するという点も、なかなかすごいアイデアですよね。

最後に余談ですが、dom氏はROSESに関連して、「on-chain npm」 というワードをツイートしています。

npmというのは、Node.jsのモジュール管理ツールのことです。
Node.jsで開発する際に、例えばnpm install threejs とするだけで、three.jsを扱えるようになります。

今回dom氏は、three.jsを9つに分割してデータコントラクトとしてデプロイしましたが、このコントラクトは誰でも使うことができます。

つまり、自分でthree.jsを使ったフルオンチェーンNFTを作ってみようと思い立った際には、dom氏のデプロイしたコントラクトから three.jsを読み込めば良いのです。

もちろんthree.jsに限らず、誰かがJavaScriptライブラリをデータコントラクトとしてデプロイすれば、それを誰もが使えるようになります。

nawoo

よって、まさにnpmのように、on-chainで誰でも簡単にJavaScriptのライブラリを扱えるようになるわけです。

フルオンチェーンNFTの新時代

今回、筆者はROSESのコードを見ていて、今後フルオンチェーンNFTは大きく変わるかもしれないと感じました。

Solidityという制約の多い言語と厳しいガスリミットの中で、いかに工夫してフルオンチェーンNFTを作成するか頭を悩ませていたのですが、three.jsやp5.jsのようなリッチなJavaScriptライブラリが使えるとなれば、実現できることは一気に増えます。

また、データコントラクトを使うことで、ファイルサイズなどの制約もかなり緩くなるでしょう。

今後、ROSESに影響された数多くのプロジェクトが出てくると思いますが、フルオンチェーンNFTがどのように進化していくのか非常に楽しみです。

編集後記

【AD】GAME OF THE LOTUS PROJECT
でりおてんちょー

「地方創生×NFT」の取り組みとして非常にユニークで面白いと思います。


どうも、イーサリアムnavi運営のでりおてんちょーです。

今回はイーサリアムnavi初の試みとして、Solidity開発者の方に依頼して「コントラクト解説記事」を書いていただきました。

Solidityの内容解説を中心とした記事は久方振りではありますが、元々イーサリアムnaviはSolidityの情報発信から始まっており、丁度1年ほど前はdiscord内で「コントラクト輪読会」という勉強会をやっていた時期もあったほどです。

出典:Twitter

しかし最近は、私自身があまり開発の勉強にまで手が回らなくなってしまったこともあり、勉強会ならびに開発にかかわる新規記事執筆ができていなかったという、非常にもどかしい状況にありました。

そんな中、立ち上げ当初からイーサリアムnaviに携わってくださっていたnawooさんが、「domが最近出したROSEのコントラクトの中身が面白かったから記事を書けます」と言ってくださったことにより、本記事の公開に至った次第です。

個人的にも興味があった内容であったと同時に、私自身も理解が深まった大変分かりやすい記事でした。

dom氏のNFTは、今までにもイーサリアムnaviでも多数取り扱っておりますが、彼の思想や表現する作品には示唆深いメッセージが多数込められていると考えているので、ぜひ本記事に興味を持たれた方は他の関連記事もご覧になってみてください。

さて、今後もイーサリアムnaviの中で私以外の方に専門記事を書いていただく機会があるかと思いますが、できるだけ難解なテーマでも皆様に分かりやすくお届けできるよう、編集スキルを磨いていきたいと考えています。

また、さらなる更新記事数の増加・対象領域の拡大を目指すべく、個人運営から法人運営に切り替えてやっていくことを決断し、現在さまざまな方面で動いている段階です。

こちらは色々と状況が落ち着き次第改めて報告したいと思いますが、今後もイーサリアムnaviのスタイル自体は変えることなく虎視眈々と靡かずやってまいりますので、読者の皆様にはこれからもご愛顧賜りますようお願い申し上げます。

イーサリアムnaviを運営するSTILL合同会社では、以下などに関するお問い合わせを受け付けております。

  • 広告掲載
  • PR記事の執筆業務
  • リサーチャー業務
  • アドバイザリー業務
  • その他(ご依頼・ご提案・ご相談など)

まずはお気軽に、ご連絡ください。

励みになるので、よかったらSNSなどでシェアしてください!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

フルオンチェーンNFTクリエイター。Lootを見たことがきっかけでNFTやSolidityにハマりました。 今までに作ったフルオンチェーンNFTは以下をご覧ください。

目次