こんにちは、フルオンチェーンNFTクリエイターのnawooです。
先日、dom氏(@dhof)が Twitter上で、新作NFTとなる「ROSES」を発表しました。
ROSES
— dom (@dhof) September 13, 2022
procedurally generated in 3d + served fully on-chain
supply 1024 (32 reserved)
NOT CURRENTLY MINTABLE. minting — like rendering — is modular & upgradable. will share details once determined
browse the first 32 https://t.co/cEmisHnJ3d
technical details coming shortly pic.twitter.com/x83melUxAJ
上のGIFアニメーションを確認していただくと『3Dのバラが回転している』ことが分かりますが、なんと驚くべきことにこちらはEthereumでの「フルオンチェーンNFT」となっています!
しかも、トークンごとに「バラの形」「色」が異なる『ジェネラティブアート』になっているのです。

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

その謎に迫るべく筆者はこの度、本NFTの元となるコントラクトコードを読んでみました。
ということで本記事では、先日dom氏が作成したフルオンチェーンNFT「ROSES」についてご紹介することで、本プロジェクトの概要ならびにコントラクトの中身、フルオンチェーンNFTの新時代を切り拓く可能性などを理解していただくことを目的とします。
でははじめに、この記事の構成について説明します。
まずは、本記事の題材となっている『ROSES』について、執筆時点で公開されている情報をもとに概要を述べてまいります。
続いて、tokenURI関数ならびに実際のHTMLの内容を確認していくことで、「ROSES」がフルオンチェーンで3Dを実現していることを確認してまいります。
最後に、STEP2で解説した「複雑なHTML」をどうやってコントラクトで作成しているかについて、解説してまいります。
本記事が、「ROSES」の概要や注目ポイント、斬新なフルオンチェーンNFTの作成方法などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。
※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。


前書き|「ROSES」とは
「ROSES」作成者のdom氏の発表によると、執筆時点においては『技術的な詳細は近日中に公開予定』となっており、現時点でNFTに関する詳細は未公表といった状況です。
それを踏まえて、本記事ではEtherscanで公開されているスマートコントラクトの情報をもとに、本NFTプロジェクトがどのように「3D×フルオンチェーン」を実現しているのかなどについて、私見を含めて解説してまいります。


- 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ファイルが指定されていることが分かります。


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



ちなみにこちらを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にも対応しています。


- three.jsという、3D描画用のJavaScriptライブラリを使っている
- 3Dのバラを作成するJavaScriptコードを作成し、HTMLに埋め込んでいる
- HTMLをdataURL化して、
animation_url
で返している
HTMLの内容について
続いて本節では、実際のHTMLの内容を確認していきます。
下記は、animation_url
をデコードして整形したものですが、5つの<script>
と1つの<style>
が出てきます。



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



次節では、この複雑なHTMLをどうやってコントラクトで作成しているかについて、一緒に見ていきましょう。
関連するコントラクトについて解説
ROSESのコントラクトは「Roses.sol」ですが、そこから呼び出されるコントラクトが多数あるので、以下の画像ならびにテーブルを用いて整理しておきます。


コントラクト | アドレス | 内容 |
---|---|---|
Roses.sol | 0x3e743377417cd7ca70dcc9bf08fac55664ed3181 | 本体 |
HelloWorldsRenderer.sol | 0x168219161C8F88DE76027173227a569544FF03c1 | tokenURIを作成する |
DataChunkCompiler.sol | 0xeC8EF4c339508224E063e43e30E2dCBe19D9c087 | HTMLのパーツを作成する |
FFlateDataChunk1.sol | 0xa942F946A35545F50792DA1Ea1ADf0c3b619b921 | データコントラクト (fflate 1 of 2) |
FFlateDataChunk2.sol | 0xF10EeDb5ACE715d78e0f89eCd1Dfc3E5874f6e3c | データコントラクト (fflate 2 of 2) |
ThreeDataChunk1.sol | 0xA32bb79b33B29e483d0949C99EC0C439b29e2B33 | データコントラクト (three.js 1 of 9) |
ThreeDataChunk2.sol | 0x0d104Dea962b090bC46c67a12e800ff16eeffB75 | データコントラクト (three.js 2 of 9) |
ThreeDataChunk3.sol | 0x1D11a1c75e439A50734AEF3469aed9ca4fFe39fc | データコントラクト (three.js 3 of 9) |
ThreeDataChunk4.sol | 0x6bAb43D4F3587f9f3ca1152C63E52BF7F8de2Dc1 | データコントラクト (three.js 4 of 9) |
ThreeDataChunk5.sol | 0x57beAe62670Ff6cCf8311411a2A2aAb453413987 | データコントラクト (three.js 5 of 9) |
ThreeDataChunk6.sol | 0xF3A95B30E1Fc2EdCea41fF93270249b6Ab979730 | データコントラクト (three.js 6 of 9) |
ThreeDataChunk7.sol | 0x52a31D845f4bdC1D47Ee21dB7C25Bde2423A91Ae | データコントラクト (three.js 7 of 9) |
ThreeDataChunk8.sol | 0x6CcCc7eA426E14F1E07528296c7d226677fd2fF6 | データコントラクト (three.js 8 of 9) |
ThreeDataChunk9.sol | 0xc230862406bBe44f499943Ae4E9E6317a95BC7Ad | データコントラクト (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にかかるガス代が非常に安くなります。
話を本題に戻しますと、ThreeDataChunk1〜ThreeDataChunk9のdata
には、three.jsのコードが分割されて入っています。



厳密には、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


コントラクト内に定数として直接書かれていますが、こちらはbase64.jsのサイズが小さいため、データコントラクトにしなかったのではないかと考えられます。
string public constant base64Utils = 'data%253Atext%252Fjavascript%253Bbase64%2 ~ Cn0%253D';
この定数は、<script src="〜">
の中に埋め込まれて、HTMLのscript 1の部分になります。
fflate


データコントラクトの 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です。



おそらく、文字列結合するとガス制約をオーバーしてしまうと思われます。
まず、gzip圧縮してBase64エンコードすると、218KBまで圧縮できます。
1つのデータコントラクトの上限は約24.5KBなので、データコントラクト9つに分割して保存しています。
データコントラクトからdata
を読み出して結合し、gzip圧縮 & Base64エンコードされたままの状態で、JavaScriptの変数に代入しておきます。
あとはJavaScriptによって、gzip展開と<script src="〜">
への埋め込みをおこなっています。
three.jsを読み込むための手順は、以下になります。
- あらかじめthree.jsをgzip圧縮して、base64エンコードしておく
- 9つに分割して、ThreeDataChunk1〜9としてデプロイする
- DataChunkCompilerでThreeDataChunk1〜9から読み出して結合し、
threejs
変数に代入する - script 3を作成する (
BEGIN_SCRIPT_DATA_COMPRESSED
+threejs
+END_SCRIPT_DATA_COMPRESSED
) - Base64デコードして、gzipを展開する
- 再びBase64エンコードをおこなってから、dataURL化する
- createElement で
<script>
タグを生成して、documentに追加する - 生成した
<script src="〜">
に、dataURLを設定する



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>



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





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





ここまでくればあと一息です。
本コントラクトの 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されており、コードが読みづらくなっています。



私は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>
「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でのシェア・コメントなどしていただけると嬉しいです。
🆕記事をアップしました🆕
— イーサリアムnavi🧭 (@ethereumnavi) September 30, 2022
今回のテーマは、先日dom氏が作成したフルオンチェーンNFT「ROSES」について✍️
ROSESは、トークンごとにバラの形や色が異なるジェネラティブアートNFT🌹
『3D×フルオンチェーン』のNFTをどのように実現したのか、詳しく解説しています⛓https://t.co/wnaFMAK8Db@dhof



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


「フルオンチェーンNFTでHTMLを使う」というアイデアは他でも見たことがありましたが、まさかthree.jsを動かしてしまうとは、本当に驚きました。
サイズが大きいファイルをgzip圧縮しておいて、JavaScriptで展開するという点も、なかなかすごいアイデアですよね。
flowchart explaining the rough process
— dom (@dhof) September 13, 2022
data contracts/sstore2 are well understood at this point so will skip that
main unlock is using compressed data, then decompressing and loading at runtime via an injected helper script in the data uri
“on-chain npm” etc should be possible pic.twitter.com/h8GQnczvLN
npmというのは、Node.jsのモジュール管理ツールのことです。
Node.jsで開発する際に、例えばnpm install three
とするだけで、three.jsを扱えるようになります。
今回dom氏は、three.jsを9つに分割してデータコントラクトとしてデプロイしましたが、このコントラクトは誰でも使うことができます。
つまり、自分でthree.jsを使ったフルオンチェーンNFTを作ってみようと思い立った際には、dom氏のデプロイしたコントラクトから three.jsを読み込めば良いのです。
もちろんthree.jsに限らず、誰かがJavaScriptライブラリをデータコントラクトとしてデプロイすれば、それを誰もが使えるようになります。



よって、まさにnpmのように、on-chainで誰でも簡単にJavaScriptのライブラリを扱えるようになるわけです。
今回、筆者はROSESのコードを見ていて、今後フルオンチェーンNFTは大きく変わるかもしれないと感じました。
Solidityという制約の多い言語と厳しいガスリミットの中で、いかに工夫してフルオンチェーンNFTを作成するか頭を悩ませていたのですが、three.jsやp5.jsのようなリッチなJavaScriptライブラリが使えるとなれば、実現できることは一気に増えます。
また、データコントラクトを使うことで、ファイルサイズなどの制約もかなり緩くなるでしょう。
編集後記


Nouns DAO JAPANは世界で一番Nounsを広げるコミュニティを目指します。Discord参加はこちら
どうも、イーサリアムnavi運営のでりおてんちょーです。
今回はイーサリアムnavi初の試みとして、Solidity開発者の方に依頼して「コントラクト解説記事」を書いていただきました。
Solidityの内容解説を中心とした記事は久方振りではありますが、元々イーサリアムnaviはSolidityの情報発信から始まっており、丁度1年ほど前はdiscord内で「コントラクト輪読会」という勉強会をやっていた時期もあったほどです。


しかし最近は、私自身があまり開発の勉強にまで手が回らなくなってしまったこともあり、勉強会ならびに開発にかかわる新規記事執筆ができていなかったという、非常にもどかしい状況にありました。
そんな中、立ち上げ当初からイーサリアムnaviに携わってくださっていたnawooさんが、「domが最近出したROSEのコントラクトの中身が面白かったから記事を書けます」と言ってくださったことにより、本記事の公開に至った次第です。
dom氏のNFTは、今までにもイーサリアムnaviでも多数取り扱っておりますが、彼の思想や表現する作品には示唆深いメッセージが多数込められていると考えているので、ぜひ本記事に興味を持たれた方は他の関連記事もご覧になってみてください。




さて、今後もイーサリアムnaviの中で私以外の方に専門記事を書いていただく機会があるかと思いますが、できるだけ難解なテーマでも皆様に分かりやすくお届けできるよう、編集スキルを磨いていきたいと考えています。
また、さらなる更新記事数の増加・対象領域の拡大を目指すべく、個人運営から法人運営に切り替えてやっていくことを決断し、現在さまざまな方面で動いている段階です。


イーサリアムnaviを運営するSTILL合同会社では、以下などに関するお問い合わせを受け付けております。
- 広告掲載
- リサーチ代行業務
- アドバイザー業務
- その他(ご依頼・ご提案・ご相談など)
まずはお気軽に、ご連絡ください。
- Webサイト:still-llc.co.jp
- Twitter:@STILL_Corp
- メールアドレス:info@still-llc.com