🦹‍♀️

CloneXのスマコンを読んでみる(1)

2022/06/23に公開1

さてはじまりました。スマコン読んでみるシリーズ行きましょう!

CloneXのソースコードはどこに?

NFT系のソースコードは以下の手順で辿ることができることが多いです。OpenSeaでNFTの個別ページに行き、左のリストでDetailsのブロックを見つけましょう。その一番上に Contract Address があります。右側の 0x***** がコントラクトアドレスになっていて、リンク先はEtherescanになります

リンク先のEtherescanに飛ぶと下記のようなTransactionが表示さてているページにいきます
https://etherscan.io/address/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b

Transactionの上にタブがあります。そこから Contractを見つけクリックしましょう

https://etherscan.io/address/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b#code

ここにはSmart Contractのコードが表示されています。これはSmartContractのデプロイした人がアップロードすることにより表示されるようになります。

上部に

Contract Source Code Verified (Exact Match)

という緑のチェックアイコンがついた文字が見えると思います。これはDeployされたこのアドレスのコントラクトとソースコードをコンパイルしたバイトコード並びにDeploy時の引数が同じことを示しています。

これがあれば、ソースコードの中身が、動作しているコントラクトと一致しているので読んで検証ができることを示しています。

少し脱線しますが….

Solidity というプログラミング言語で Ethereumのスマートコントラクトは記述されることが多いです。プログラミング言語は人間が読みやすいよう、メンテナンスしやすいように記載をしますが、実際に動作させる前にコンパイルと呼ばれる変換が行われ、コンピューターが使いやすいバイトコードという形に変換されてEthereumのネットワークに乗るわけです

さて話を戻しましょう。僕らは人間の読めるソースコードを読むことに集中です。だって、バイトコードとソースコードは一致することが証明されていますもん。Etherescanが信用できないときは自分でコンパイルして比較してもいいわけです。でもこのあたりも、Etherescanが一致しないものを一致しているという虚偽をするかどうかは衆目の力によって安全度が高い状態ではないかなと思います。オープンソースのなせる技ですね

では実際にソースコードを見ていきましょう。15個のファイルがアップロードされていますが、一個目の clonex.solが本体です。長いですが、ちゃんと説明しますのでスクロールしてください。

File 1 of 15 : clonex.sol

clonex.sol
// SPDX-License-Identifier: MIT

/*
    RTFKT Legal Overview [https://rtfkt.com/legaloverview]
    1. RTFKT Platform Terms of Services [Document #1, https://rtfkt.com/tos]
    2. End Use License Terms
    A. Digital Collectible Terms (RTFKT-Owned Content) [Document #2-A, https://rtfkt.com/legal-2A]
    B. Digital Collectible Terms (Third Party Content) [Document #2-B, https://rtfkt.com/legal-2B]
    C. Digital Collectible Limited Commercial Use License Terms (RTFKT-Owned Content) [Document #2-C, https://rtfkt.com/legal-2C]
    
    3. Policies or other documentation
    A. RTFKT Privacy Policy [Document #3-A, https://rtfkt.com/privacy]
    B. NFT Issuance and Marketing Policy [Document #3-B, https://rtfkt.com/legal-3B]
    C. Transfer Fees [Document #3C, https://rtfkt.com/legal-3C]
    C. 1. Commercialization Registration [https://rtfkt.typeform.com/to/u671kiRl]
    
    4. General notices
    A. Murakami Short Verbiage – User Experience Notice [Document #X-1, https://rtfkt.com/legal-X1]
*/

pragma solidity ^0.8.2;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

abstract contract CloneXRandomizer {
    function getTokenId(uint256 tokenId) public view virtual returns(string memory);
}

contract CloneX is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("CloneX", "CloneX") {
        _tokenIdCounter.increment(); // Making sure we start at token ID 1
    }
    
    event CloneXRevealed(uint256 tokenId, string fileId); // Sending the event for offchain script to transform the right file
    
    address randomizerAddress; // Approved randomizer contract
    address mintvialAddress; // Approved mintvial contract
    string public _tokenUri = "https://clonex-assets.rtfkt.com/"; // Initial base URI
    
    bool public contractLocked = false;

    function mintTransfer(address to) public returns(uint256) {
        require(msg.sender == mintvialAddress, "Not authorized");
        
        CloneXRandomizer tokenAttribution = CloneXRandomizer(randomizerAddress);
        
        string memory realId = tokenAttribution.getTokenId(_tokenIdCounter.current());
        uint256 mintedId =  _tokenIdCounter.current();
        
        _safeMint(to, _tokenIdCounter.current());
        emit CloneXRevealed(_tokenIdCounter.current(), realId);
        _tokenIdCounter.increment();
        return mintedId;
    }

    // Change the randomizer address contract
    function setRandomizerAddress(address newAddress) public onlyOwner {
        randomizerAddress = newAddress;
    }
    
    // Change the mintvial address contract
    function setMintvialAddress(address newAddress) public onlyOwner { 
        mintvialAddress = newAddress;
    }
    
    function secureBaseUri(string memory newUri) public onlyOwner {
        require(contractLocked == false, "Contract has been locked and URI can't be changed");
        _tokenUri = newUri;
    }
    
    function lockContract() public onlyOwner {
        contractLocked = true;   
    }
    
	/*
	 * Helper function
	 */
	function tokensOfOwner(address _owner) external view returns (uint256[] memory) {
		uint256 tokenCount = balanceOf(_owner);
		if (tokenCount == 0) return new uint256[](0);
		else {
			uint256[] memory result = new uint256[](tokenCount);
			uint256 index;
			for (index = 0; index < tokenCount; index++) {
				result[index] = tokenOfOwnerByIndex(_owner, index);
			}
			return result;
		}
	}

    /** OVERRIDES */
    function _baseURI() internal view override returns (string memory) {
        return _tokenUri;
    }
    
	function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }
}

Solidityのスマートコントラクトはとても読みやすい構造です。特にCloneXのスマートコントラクトはとてもシンプルなのでまず全体像を把握するために骨だけにしてみましょう。

pragma solidity ^0.8.2;

contract Clonex {
    constructor() ERC721("CloneX", "CloneX"){}
+   function mintTransfer(){}
    function setRandomizerAddress(){}
    function setMintvialAddress(){}
    function secureBaseUri(){}
    function lockContract(){}
    function tokensOfOwner(){}
    function _baseURI(){}
    function _beforeTokenTransfer(){}
    function _burn(){}
    function tokenURI(){}
    function supportsInterface(){}
}

Solidityのプログラムのブロックは 波かっこ {}によって囲みます。{}は多重化できます。function(){}というもう一段下のブロックも見えるかと思います。 Contract{}の中に function{}をいっぱい並べてSolidityは記述していきます。

ここで気がついた方もいらっしゃると思いますが、この function(){}の名前は実は直コンする時につかうものなんですね。あか丸をつけた名前が一番の上のfunction()と同じ名前ですよね。実はEtherescanでコントラクトを叩くとこの関数の中にある作業を実行するのです。

そして、このmintTransfer()こそ CloneXのNFTを生成するコアの部分になるんですね

mintTransfer 関数を読む

function mintTransfer(address to) public returns(uint256) {
    require(msg.sender == mintvialAddress, "Not authorized");

    CloneXRandomizer tokenAttribution = CloneXRandomizer(randomizerAddress);

    string memory realId =      
        tokenAttribution.getTokenId(_tokenIdCounter.current());
    uint256 mintedId =  _tokenIdCounter.current();

    _safeMint(to, _tokenIdCounter.current());
    emit CloneXRevealed(_tokenIdCounter.current(), realId);
    _tokenIdCounter.increment();
    return mintedId;
}

まさに心臓部。ここを読んでいきましょう!

Solidityでのfunction の記法は、比較的新しい言語体系のため戻り値を後ろに記載します

function mintTransfer(address to) public returns(uint256)

funciotn というキーワードで始め
mintTransferと関数名を指定
()内に与えるパラメーターを記載します

()内の address to は、 adress という型を持つ toという変数にもらったパラメーターを入れることを示しています。address はコントラクトやウオレットで使うEthereum Addressとなります 0x**** おなじみのやつですね。Solidityは専用言語のため予めaddressを扱う型が用意されているわけです

publicは、contract外からのこの関数を使っても良いという印です

returns(uint256)
この関数が返す値は uint256の型を持つことがここで宣言されています。これまた近代言語のため複数の戻り値を返すことも可能です。今回の uint256は、符号なしのゼロと正の整数を256bitで表現できる数の数字を示しています。

では次の行に行きましょ。ここからが関数の処理部分です
require(msg.sender == mintvialAddress, "Not authorized");

solidityでは requireを使い実際にこの関数を実行するのかチェックをします。()の中に,を区切りに2つのパラメーターが与えられています
msg.sender == mintvialAddress
これは、この関数の実行者が mintvialAddressの実行者と同じであるかを示しています。最初から普通ではない比較ですね。これは CloneXだからおこることですので、CloneXを知らない方のために説明すると

CloneXは Vialというカプセルの形で販売が行われました。その後、所有者がCloneXのチームが用意しているWebサイトでリビールという作業を行うと、右のようにCloneXのNFTを手に入れることができます。

ということでCloneXをMintするにはVialが必要であり、実際に人間は直接CloneXのMintをできるのではなく、Vialのスマートコントラクトでリビール作業を行うと Vialのスマートコントラクトが、mintTransfer()関数を叩いている構図となるわけです

mintVial on Ethescan

ここで、vialのコントラクト以外がこの関数を実行しようとすると1つめのパラメーターの条件を満たさず、そこでTransactionはストップします。スマコンのTrは2つめのパラメータのエラーを返して終了します。

ということで requre(条件,エラーメッセージ)という構文はSolidityではよくみられます。また、実行条件の判定で使っているためNFTハンターの方はここをちゃんとチェックする必要があるわけです。mintVialの方もみていく必要もありそうですね。この記事が好評だったら続編として書いてみたいと思います。自分で読んでみるのもいいでしょう!

CloneXRandomizer tokenAttribution = CloneXRandomizer(randomizerAddress);

ここはこの後使うために自分で用意した外部スマコンの CloneXRandomizerを使う準備をしています。Randomizerなのでランダム化してどのCloneXが手に入るのかわからないようにする仕組みがこの中人あることが想定できますね。ここも、この記事が好評だったら続編として書いてみたいと思います。自分で読んでみるのもいいでしょう!

        tokenAttribution.getTokenId(_tokenIdCounter.current());

でこちらが実際に使用しているところですね。ここでCloneXRandomizerからランダムなIDをもらっています。ここが、くじ引きをしているところだと思ってください。これがあるので自分がMintするCloneXが何になるのかわからないわけです。

さらに注目は _tokenIdCounter.current()をパラメータとして与えています。この_tokenCounterはCloneXのスマコン内に設置されたカウンターになるので、実は...どのタイミングでCloneXにリビールするかによって運命が左右されることがわかります。なんとーー!

uint256 mintedId = _tokenIdCounter.current();
mintedIdを関数の戻り値をして返すために書いているところですね。

_safeMint(to, _tokenIdCounter.current());
emit CloneXRevealed(_tokenIdCounter.current(), realId);

そしてここがまさにMintをしているところです。 to にはvialの所有者のアドレスが記載されておりMintされたCloneXがあなたのものになるわけです。

あれあれ。さっきRandomizerはどうなったのかな...?mint関数には上げていませんね。emitでログの部分には記載がありますが... emitの宣言のところにいってみると書いてありました!

event CloneXRevealed(uint256 tokenId, string fileId); // Sending the event for offchain script to transform the right file

Sending the event for offchain script to transform the right file

と書いてあります。event はログ出力の形式を指定するところです。ログの出力を利用してオフィチェーンでランダムくじ引きされた数字を使ってCloneXの割り当てをおこなっているということでしょう。オフチェーンなのでその先はわかりません。

    _tokenIdCounter.increment();
    return mintedId;

ということで最後にtokenIdを1つ増やして。mintedIdを戻して終わりです。

お疲れ様です!これであなたのCloneXが生まれました。

と長くなりましたので一旦パート1はここで終了します。これ以外の関数について次回は見ていきましょう。

Discussion

かみぶくろかみぶくろ

最高の記事ありがとうございます!
Solidity勉強中で、NFTにどのようなギミックを作れるかなあ...と研究してたところでした!でも自分には難しくて😩続編あったら嬉しいです!