📘

Sui NetworkとSui Moveを理解する!!

2023/07/23に公開

1.png

皆さん、こんにちは!

先日、 日本で初めて Sui Move というプログラミング言語を主役にしたハッカソン、 Move Hackathon in WebXに参加いたしました!

https://app.akindo.io/hackathons/q3OnoAj86Tjj1D7P4

その一環で Sui Networkと Sui Move言語について勉強したのでそのまとめを記事にしてみました!!

最後まで読んでいただければと思います!!

なお、今回使用するソースコードは下記にまとめています。

https://github.com/mashharuki/Sui-NFT-Dapp

なお、AptosとMoveについては以前記事を書いていますのでよろしければそちらもご覧ください。

https://zenn.dev/mashharuki/articles/49359206592aa8

目次

  1. Sui Networkと Sui Moveの概要
  2. 今回理解するために作成した 簡易 NFT マーケットプレイスアプリ
  3. 開発したスマートコントラクトの概要
  4. Solidityで作るNFTとの違い
  5. まとめ
  6. 参考文献

1. Sui Networkと Sui Moveの概要

Sui Networkの概要

まずは、Sui Networkの概要からまとめていきます!!

https://sui.io/

Sui Networkは、EthereumやXRPL、Astar Networkなどと同じくレイヤー1ブロックチェーンに分類されます。

メインネットは、2023年5月に始動したばかりの新興レイヤー1ブロックチェーンとなります!!

https://coinpost.jp/?p=456931

大元はMeta社が実現させようとしていたDiemであり、facebookが抱える10億人規模のユーザーからのトランザクションを処理することも視野に入れた設計となっています。コンセンサスアルゴリズムの仕組みなどはEthereumとは全く異なったアプローチをとっており、ざっくりいうとトランザクションの並列処理が可能となっているブロックチェーンです。

diem_log.png

ちなみに、AptosとSuiどちらのチェーンもコンセンサスメカニズムとしてProof of Stake(PoS)を採用しています。

実際にウォレットからSuiを送金してみたりすると分かるのですが、処理のスピードは他のチェーンと比較して段違いに早いです!

これから参加するであろう10億人のユーザーに向けて広めることを目指して設計されており、
1秒あたりのトランザクション処理数(TPS)が30万件(理論速度)に及ぶ、処理能力の高さを特徴としている。

(上記の文言は https://coinpost.jp/?p=456931 より引用)

資金調達もそれなりの額を達成していますし、テストネットの段階でもそれなりの数のプロジェクトがSui上で動いていたので注目度も伺えます!

トランザクションの並列処理と簡単に記載しましたが、その根幹技術は「Narwhal and Tusk」という名前がついています。

Narwhal and Tusk: A DAG-based Mempool and Efficient BFT Consensus

論文は下記です。

https://arxiv.org/abs/2105.11827

その名の通り、 DAG(Directed Acyclic Graph, 有向非巡回グラフ)をベースにしたMempoolを設計したことにより性能を向上させることに成功しています。

https://qiita.com/maebaru/items/5b6cb981777624ab843c

Sui Moveの概要

Sui Moveは、Sui Network上で動くスマートコントラクトを開発するためのプログラミング言語です!!

Aptosで使われているMove言語をベースにしており、 Move言語をさらに改修された言語となっております。
2つの言語の具体的な違いについて以前オンラインで行われた勉強会にて Umi Protocolのチームから解説していただきました。

Move言語はRust言語をベースにしているので見た目やフォルダ構造はRustに似ています!!
なのでNearやSolanaの開発者の方は入りやすいと思います!
※ EVM系のチェーンしか触っていない開発者は最初は大変かもしれません。

move_suimve.png

(https://docs.google.com/presentation/d/1-pyR9_DUrFu6a2LXDo7JgFBLIGA0XAHUnPlaAveoYhA/edit#slide=id.g25903865a1f_1_17 より抜粋)

オブジェクトモデルが異なっており、Move言語はアカウントセントリック、SuiMoveはオブジェクトセントリックとなっています。SuiのオブジェクトはグローバルにユニークなIDを持つので 全てがNFT!! といっても過言ではないかもしれません。

この辺からしてEthereumとは全く異なるアプローチを取っていることがわかります。

そんな Sui Moveを使って簡易NFTマーケットプレイスアプリを開発してみましたのでその内容を共有していきます!

2. 今回理解するために作成した 簡易 NFT マーケットプレイスアプリ

今回作成したアプリは下記のように、NFTをミント、一覧取得、Transferできる比較的シンプルなアプリになっています!一見するとEVM系のチェーンで作ったのとあまり変わらないUIになりますね。

nft_dapp.png

mint NFTボタンからNFTをミントすることができ、 get nftsでミントしたNFTの一覧を取得することができます!
※ 今回はサンプルなので名前や説明文、画像データは全て事前に指定したものになっています。

簡単に動かし方も解説していきます!!

  • gitのクローン

    git clone https://github.com/mashharuki/Sui-NFT-Dapp.git  
    
  • npm モジュールのインストール

    pnpm install
    
  • フロントエンド(Next.js)の起動

    pnpm run frontend dev
    

ここで問題なければフロントエンドが起動するはずなので http://localhost:3000 にアクセスしましょう!!

まだウォレットが接続されていないので左上のボタンを押してConnect Walletします!!

connectWallet_1.png

ポップアップが表示されるので、接続したいウォレットアプリを選択しましょう!!!

connectWallet_2.png

承認します!このへんの流れはEVM系と変わらずです。

完了したら 左側のget nfts ボタンで接続しているアカウントに紐づいているNFTオブジェクトを一覧で取得します!

nft_dapp.png

NFTのオブジェクトの取得については、Suiの公式ドキュメントに従ってAPIを利用して取得しています!

ソースコードだと以下のように実装しています!

// Request data
  // Connect walletしているアドレスに紐づくすべての資産(オブジェクト)を取得する。
  const requestData = {
    jsonrpc: '2.0',
    id: 1,
    method: 'suix_getOwnedObjects',
    params: [
      address,
      {
        "filter": {
          "MatchAll": [
            {
              "StructType": `${NFT_PACKAGE_ID}::dev_nft::DevNFT`
            }
          ]
        },
        options: {
          showType: true,
          showOwner: true,
          showPreviousTransaction: true,
          showDisplay: false,
          showContent: true,
          showBcs: false,
          showStorageRebate: false,
        },
      },
    ],
  };

  // Fetch options
  const requestOptions = {
    method: 'POST', 
    headers: {
      'Content-Type': 'application/json', 
    },
    body: JSON.stringify(requestData), 
  };

  // Make the API request
  fetch(RPC_API_URL, requestOptions)
    .then(response => {
      // Check if the response was successful (status code in the range of 200-299)
      if (response.ok) {
        return response.json(); // Parse the response data as JSON
      } else {
        throw new Error(`Request failed with status code:${response.status}`);
      }
    })
    .then(responseData => {
      // Handle the response data
      console.log('Response:', responseData.result.data);
      setNfts(responseData.result.data);
      // Perform any necessary processing or display based on the response
    })
    .catch(error => {
      // Handle any errors that occurred during the request
      console.error('Error:', error);
      return null;
    });

初めて接続した場合にはNFTを持っていないはずなのでミントしてみましょう!!
左側のボタンを押してミントします!!

ポップアップが表示されるので Approveをボタンを押してトランザクションの処理を承認します。

mint_2.png

成功しました!

mint_1.png

再度、get nftsボタンを押すとオブジェクトが取得できるはずです!!

nft_dapp.png

では、最後に適当なアドレスにNFTを送ってみましょう!!
アドレスを入力したら Tranfer NFTボタンを押します!

transfer_1.png

Transfer NFTボタンを押すと入力したアドレスにNFTのオブジェクトを実際に渡すことができます!!

大事なことなので2回書きます。

入力したアドレスにNFTのオブジェクトを実際に渡すことができます!!

後ほど詳しく記載しますが、ここがSolidityで作るNFTとの一番の違いになってきます!!

ポップアップが表示されるので Approveをボタンを押してトランザクションの処理を承認します。

transfer_2.png

成功しました!

transfer_3.png

ここでNFTのオブジェクトを実際にtransferさせた時のトランザクションの記録も共有していきたいと思います!

https://suiexplorer.com/txblock/HQAPjfZjCLSuQcgs38p9iKxFhR6HLbsQaioDVTcgShgP?network=testnet

NFTはオブジェクトとして存在し、それが送信元アドレスから宛先アドレスにデジタルアセットとしてtransferさせられています!!

再度get nftsボタンを押すと何も表示されくなります!

transfer_4.png

3. 開発したスマートコントラクトの概要

それでは今回開発したスマートコントラクトのソースコードを見ていきたいと思います!

Rustをベースにしているので似ています。

シンプルな構成なのであまりコメントは振っていませんが、ざっくり解説すると下記のような構造になっています。

① モジュールのインポート
② コントラクトで使うオブジェクトのデータ構造の定義 (今回は、DevNFTという名前にしています。)
③ イベントの定義
④ Setter関数群
⑤ Getter関数群
⑥ テストコード


/**
 * Simple NFT Contract
 */
module nft::dev_nft {
    use sui::url::{Self, Url};
    use std::string;
    use sui::object::{Self, ID, UID};
    use sui::event;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    /// A NFT that can be minted by anybody
    struct DevNFT has key, store {
        id: UID,
        /// Name for the token
        name: string::String,
        /// Description of the token
        description: string::String,
        /// URL for the token
        url: Url,
    }

    /// Mint Event 
    struct MintNFTEvent has copy, drop {
        // The Object ID of the NFT
        object_id: ID,
        // The creator of the NFT
        creator: address,
        // The name of the NFT
        name: string::String,
    }

    /// Transfer Evnet
    struct TransfertNFTEvent has copy, drop {
        // The Object ID of the NFT
        object_id: ID,
        // from address
        from: address,
        // to address
        to: address,
    }

    /// Create a new dev_nft
    public entry fun mint(
        name: vector<u8>,
        description: vector<u8>,
        url: vector<u8>,
        ctx: &mut TxContext
    ) {
        // create DevNFT Object
        let nft = DevNFT {
            id: object::new(ctx),
            name: string::utf8(name),
            description: string::utf8(description),
            url: url::new_unsafe_from_bytes(url)
        };
        // get sender info
        let sender = tx_context::sender(ctx);
        // emit event
        event::emit(MintNFTEvent {
            object_id: object::uid_to_inner(&nft.id),
            creator: sender,
            name: nft.name,
        });
        // transfer nft to sender
        transfer::public_transfer(nft, sender);
    }

    /// transfer method
    public entry fun transfer(
        nft: DevNFT, 
        recipient: address, 
        _: &mut TxContext
    ) {
        // transfer NFT Object
        transfer::public_transfer(nft, recipient)
    }

    /// Update the `description` of `nft` to `new_description`
    public entry fun update_description(
        nft: &mut DevNFT,
        new_description: vector<u8>,
    ) {
        nft.description = string::utf8(new_description)
    }

    /// Permanently delete `nft`
    public entry fun burn(nft: DevNFT) {
        let DevNFT { id, name: _, description: _, url: _ } = nft;
        // delete NFT
        object::delete(id)
    }

    /// Get the NFT's `name`
    public fun name(nft: &DevNFT): &string::String {
        &nft.name
    }

    /// Get the NFT's `description`
    public fun description(nft: &DevNFT): &string::String {
        &nft.description
    }

    /// Get the NFT's `url`
    public fun url(nft: &DevNFT): &Url {
        &nft.url
    }
}

#[test_only]
module nft::dev_nftTests {
    use nft::dev_nft::{Self, DevNFT};
    use sui::test_scenario as ts;
    use sui::transfer;
    use std::string;

    #[test]
    fun mint_transfer_update() {
        let addr1 = @0xA;
        let addr2 = @0xB;
        // create the NFT
        let scenario = ts::begin(addr1);

        {
            dev_nft::mint(
              b"test", 
              b"a test", 
              b"https://www.sui.io", 
              ts::ctx(&mut scenario)
            )
        };
        // send it from A to B
        ts::next_tx(&mut scenario, addr1);
        
        {
            let nft = ts::take_from_sender<DevNFT>(&mut scenario);
            transfer::public_transfer(nft, addr2);
        };
        // update its description
        ts::next_tx(&mut scenario, addr2);
        {
            let nft = ts::take_from_sender<DevNFT>(&mut scenario);
            dev_nft::update_description(&mut nft, b"a new description") ;
            assert!(*string::bytes(dev_nft::description(&nft)) == b"a new description", 0);
            ts::return_to_sender(&mut scenario, nft);
        };
        // burn it
        ts::next_tx(&mut scenario, addr2);
        {
            let nft = ts::take_from_sender<DevNFT>(&mut scenario);
            dev_nft::burn(nft)
        };
        ts::end(scenario);
    }
}

動かし方はGitHubのREADMEにも記載されているのでそちらをご覧ください!!

https://github.com/mashharuki/Sui-NFT-Dapp/blob/main

今回開発したスマートコントラクトは、下記の通りデプロイされています!

https://suiexplorer.com/object/0xc67af7a8c787d95c0d4ce650cbd46022cdb6b917faa0ee1020b2fb4719c95858?module=dev_nft&network=testnet

「Package ID」ってなんだ?? って思われた方いると思います。

Sui Move言語で作成したスマートコントラクトでは、デプロイ後はロジック部分がPackage として存在しそのIDとメソッド名を指定することでスマートコントラクトの処理をフロントエンドからも呼び出せるようになります!!

特定のSDKを用いることで、ABIファイルがなくても利用できるのでEVM系のDappよりも利用しやすいですね!!

今回の実装ではSui-NFT-Dapp/pkgs/frontend/src/suitterLib/moveCall/配下にある index.tsファイルにその実装をまとめています。

パッケージIDとモジュール名(スマートコントラクトの名前)とメソッド名、引数を指定してあげればOKです!

import { TransactionBlock } from '@mysten/sui.js';
import { NFT_PACKAGE_ID } from 'src/config/constants';


/**
 * NFTを発行するためのメソッド
 */
export const moveCallMintNft = async (props: {
  tx: TransactionBlock,
  name: string,
  description: string,
  url: string
}) => {
  const { tx } = props;
  // モジュール名と関数を指定
  const moduleName = "dev_nft";
  const methodName = "mint";

  // パッケージIDを指定する。
  tx.moveCall({
    target: `${NFT_PACKAGE_ID}::${moduleName}::${methodName}`,
    arguments: [
      tx.pure(props.name),
      tx.pure(props.description),
      tx.pure(props.url),
    ],
  });
};

/**
 * NFTを移転するためのメソッド
 */
export const moveCallTransferNft = async (props: {
  tx: TransactionBlock,
  id: string,
  toAddress: string
}) => {
  const { tx } = props;
  // モジュール名と関数を指定
  const moduleName = "dev_nft";
  const methodName = "transfer";

  console.log("tx:", props.id);
  console.log("toAddress:", props.toAddress);

  tx.moveCall({
    target: `${NFT_PACKAGE_ID}::${moduleName}::${methodName}`,
    arguments: [
      tx.object(props.id),
      tx.object(props.toAddress),
    ],
  });
};

シンプルですね!!

私も最初こそ苦戦しましたが、実際にコードを書いて作ってみると開発しやすいことに気がつきました! ライブラリの種類や実装例はEthereumほど多くはないのですがこれから注目されていきそうだなと感じています!!

4. Solidityで作るNFTとの違い

では最後に一番重要なポイントとしてSolidityで作ったNFTとどのように異なるのかを見ていきたいと思います。

比較しやすいように概念図を作ってみました!!

SuiMoveとSoliditynのNFt比較.drawio.png

はい、これが全てになります!!

つまり、Solidityで作成したNFTではトークンID〇〇番のownerアドレスは××アドレスですよ! ということがMapping変数として管理されているだけであり、Transferの処理内容の本質部分はownerアドレスの内容を更新するだけとなっています!

比較用にERC721のソースコードを見てみます。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol)

pragma solidity ^0.8.19;

import {IERC721} from "./IERC721.sol";
import {IERC721Receiver} from "./IERC721Receiver.sol";
import {IERC721Metadata} from "./extensions/IERC721Metadata.sol";
import {Context} from "../../utils/Context.sol";
import {Strings} from "../../utils/Strings.sol";
import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol";
import {IERC721Errors} from "../../interfaces/draft-IERC6093.sol";

/**
 * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
 * the Metadata extension, but not including the Enumerable extension, which is available separately as
 * {ERC721Enumerable}.
 */
abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {
    using Strings for uint256;

    // Token name
    string private _name;

    // Token symbol
    string private _symbol;

    // Mapping from token ID to owner address
    mapping(uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping(address => uint256) private _balances;

    // Mapping from token ID to approved address
    mapping(uint256 => address) private _tokenApprovals;

    // Mapping from owner to operator approvals
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    /**
     * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
     */
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC721-balanceOf}.
     */
    function balanceOf(address owner) public view virtual returns (uint256) {
        if (owner == address(0)) {
            revert ERC721InvalidOwner(address(0));
        }
        return _balances[owner];
    }

    /**
     * @dev See {IERC721-ownerOf}.
     */
    function ownerOf(uint256 tokenId) public view virtual returns (address) {
        address owner = _ownerOf(tokenId);
        if (owner == address(0)) {
            revert ERC721NonexistentToken(tokenId);
        }
        return owner;
    }

    /**
     * @dev See {IERC721Metadata-name}.
     */
    function name() public view virtual returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC721Metadata-symbol}.
     */
    function symbol() public view virtual returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     */
    function tokenURI(uint256 tokenId) public view virtual returns (string memory) {
        _requireMinted(tokenId);

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenId.toString()) : "";
    }

    /**
     * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
     * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
     * by default, can be overridden in child contracts.
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }

    /**
     * @dev See {IERC721-approve}.
     */
    function approve(address to, uint256 tokenId) public virtual {
        address owner = ownerOf(tokenId);
        if (to == owner) {
            revert ERC721InvalidOperator(owner);
        }

        if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) {
            revert ERC721InvalidApprover(_msgSender());
        }

        _approve(to, tokenId);
    }

    /**
     * @dev See {IERC721-getApproved}.
     */
    function getApproved(uint256 tokenId) public view virtual returns (address) {
        _requireMinted(tokenId);

        return _tokenApprovals[tokenId];
    }

    /**
     * @dev See {IERC721-setApprovalForAll}.
     */
    function setApprovalForAll(address operator, bool approved) public virtual {
        _setApprovalForAll(_msgSender(), operator, approved);
    }

    /**
     * @dev See {IERC721-isApprovedForAll}.
     */
    function isApprovedForAll(address owner, address operator) public view virtual returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    /**
     * @dev See {IERC721-transferFrom}.
     */
    function transferFrom(address from, address to, uint256 tokenId) public virtual {
        if (!_isApprovedOrOwner(_msgSender(), tokenId)) {
            revert ERC721InsufficientApproval(_msgSender(), tokenId);
        }

        _transfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual {
        if (!_isApprovedOrOwner(_msgSender(), tokenId)) {
            revert ERC721InsufficientApproval(_msgSender(), tokenId);
        }
        _safeTransfer(from, to, tokenId, data);
    }

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * `data` is additional data, it has no specified format and it is sent in call to `to`.
     *
     * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
     * implement alternative mechanisms to perform token transfer, such as signature-based.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual {
        _transfer(from, to, tokenId);
        if (!_checkOnERC721Received(from, to, tokenId, data)) {
            revert ERC721InvalidReceiver(to);
        }
    }

    /**
     * @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist
     */
    function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
        return _owners[tokenId];
    }

    /**
     * @dev Returns whether `tokenId` exists.
     *
     * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
     *
     * Tokens start existing when they are minted (`_mint`),
     * and stop existing when they are burned (`_burn`).
     */
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _ownerOf(tokenId) != address(0);
    }

    /**
     * @dev Returns whether `spender` is allowed to manage `tokenId`.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
        address owner = ownerOf(tokenId);
        return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
    }

    /**
     * @dev Safely mints `tokenId` and transfers it to `to`.
     *
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeMint(address to, uint256 tokenId) internal virtual {
        _safeMint(to, tokenId, "");
    }

    /**
     * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
     * forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
     */
    function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {
        _mint(to, tokenId);
        if (!_checkOnERC721Received(address(0), to, tokenId, data)) {
            revert ERC721InvalidReceiver(to);
        }
    }

    /**
     * @dev Mints `tokenId` and transfers it to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
     *
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - `to` cannot be the zero address.
     *
     * Emits a {Transfer} event.
     */
    function _mint(address to, uint256 tokenId) internal virtual {
        if (to == address(0)) {
            revert ERC721InvalidReceiver(address(0));
        }
        if (_exists(tokenId)) {
            revert ERC721InvalidSender(address(0));
        }

        _beforeTokenTransfer(address(0), to, tokenId, 1);

        // Check that tokenId was not minted by `_beforeTokenTransfer` hook
        if (_exists(tokenId)) {
            revert ERC721InvalidSender(address(0));
        }

        unchecked {
            // Will not overflow unless all 2**256 token ids are minted to the same owner.
            // Given that tokens are minted one by one, it is impossible in practice that
            // this ever happens. Might change if we allow batch minting.
            // The ERC fails to describe this case.
            _balances[to] += 1;
        }

        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);

        _afterTokenTransfer(address(0), to, tokenId, 1);
    }

    /**
     * @dev Destroys `tokenId`.
     * The approval is cleared when the token is burned.
     * This is an internal function that does not check if the sender is authorized to operate on the token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     *
     * Emits a {Transfer} event.
     */
    function _burn(uint256 tokenId) internal virtual {
        address owner = ownerOf(tokenId);

        _beforeTokenTransfer(owner, address(0), tokenId, 1);

        // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook
        owner = ownerOf(tokenId);

        // Clear approvals
        delete _tokenApprovals[tokenId];

        // Decrease balance with checked arithmetic, because an `ownerOf` override may
        // invalidate the assumption that `_balances[from] >= 1`.
        _balances[owner] -= 1;

        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);

        _afterTokenTransfer(owner, address(0), tokenId, 1);
    }

    /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *  As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     *
     * Emits a {Transfer} event.
     */
    function _transfer(address from, address to, uint256 tokenId) internal virtual {
        address owner = ownerOf(tokenId);
        if (owner != from) {
            revert ERC721IncorrectOwner(from, tokenId, owner);
        }
        if (to == address(0)) {
            revert ERC721InvalidReceiver(address(0));
        }

        _beforeTokenTransfer(from, to, tokenId, 1);

        // Check that tokenId was not transferred by `_beforeTokenTransfer` hook
        owner = ownerOf(tokenId);
        if (owner != from) {
            revert ERC721IncorrectOwner(from, tokenId, owner);
        }

        // Clear approvals from the previous owner
        delete _tokenApprovals[tokenId];

        // Decrease balance with checked arithmetic, because an `ownerOf` override may
        // invalidate the assumption that `_balances[from] >= 1`.
        _balances[from] -= 1;

        unchecked {
            // `_balances[to]` could overflow in the conditions described in `_mint`. That would require
            // all 2**256 token ids to be minted, which in practice is impossible.
            _balances[to] += 1;
        }

        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);

        _afterTokenTransfer(from, to, tokenId, 1);
    }

    /**
     * @dev Approve `to` to operate on `tokenId`
     *
     * Emits an {Approval} event.
     */
    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ownerOf(tokenId), to, tokenId);
    }

    /**
     * @dev Approve `operator` to operate on all of `owner` tokens
     *
     * Emits an {ApprovalForAll} event.
     */
    function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
        if (owner == operator) {
            revert ERC721InvalidOperator(owner);
        }
        _operatorApprovals[owner][operator] = approved;
        emit ApprovalForAll(owner, operator, approved);
    }

    /**
     * @dev Reverts if the `tokenId` has not been minted yet.
     */
    function _requireMinted(uint256 tokenId) internal view virtual {
        if (!_exists(tokenId)) {
            revert ERC721NonexistentToken(tokenId);
        }
    }

    /**
     * @dev Private function to invoke {IERC721Receiver-onERC721Received} on a target address.
     * The call is not executed if the target address is not a contract.
     *
     * @param from address representing the previous owner of the given token ID
     * @param to target address that will receive the tokens
     * @param tokenId uint256 ID of the token to be transferred
     * @param data bytes optional data to send along with the call
     * @return bool whether the call correctly returned the expected magic value
     */
    function _checkOnERC721Received(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) private returns (bool) {
        if (to.code.length > 0) {
            try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
                return retval == IERC721Receiver.onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert ERC721InvalidReceiver(to);
                } else {
                    /// @solidity memory-safe-assembly
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting and burning. If {ERC721Consecutive} is
     * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1.
     *
     * Calling conditions:
     *
     * - When `from` and `to` are both non-zero, ``from``'s tokens will be transferred to `to`.
     * - When `from` is zero, the tokens will be minted for `to`.
     * - When `to` is zero, ``from``'s tokens will be burned.
     * - `from` and `to` are never both zero.
     * - `batchSize` is non-zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {}

    /**
     * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is
     * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1.
     *
     * Calling conditions:
     *
     * - When `from` and `to` are both non-zero, ``from``'s tokens were transferred to `to`.
     * - When `from` is zero, the tokens were minted for `to`.
     * - When `to` is zero, ``from``'s tokens were burned.
     * - `from` and `to` are never both zero.
     * - `batchSize` is non-zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {}

    /**
     * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override.
     *
     * WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant
     * being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such
     * that `ownerOf(tokenId)` is `a`.
     */
    // solhint-disable-next-line func-name-mixedcase
    function __unsafe_increaseBalance(address account, uint256 value) internal {
        _balances[account] += value;
    }
}

つまり、全てデータの動きとしてはNFTコントラクト内で完結しています!!

実際にNFTのデータオブジェクトが作成されているわけでもなく、単にコントラクト内で管理されているデータが更新されるだけとなります! 残高情報すらコントラクにアクセスしないと分からないです。でもそのトークンIDにメタデータで色々情報を付与することでUIを豪華にしてあたかも本当にデジタル上にオブジェクトとして存在するかのようなUXを作り出すことに成功しています!! OpenSeaとかはやっぱりすごいですね!!

この辺がNFTを初めて開発する多くの開発者を誤解させている一番の理由だと考えています。
※ 私も初めてNFTを実装した時は少し困惑したのを覚えています・・。

それに対してSui Moveで作ったNFTコントラクトでは(というかすべてのスマートコントラクトでは)、あらかじめ定義されたデータ構造に従いオブジェクトが生成され実際にアカウント間でやり取りが発生します!!

sui_object.png

(https://docs.google.com/presentation/d/1-pyR9_DUrFu6a2LXDo7JgFBLIGA0XAHUnPlaAveoYhA/edit#slide=id.g17f7fd7f86c_0_187 より抜粋)

ここは、Suiのチームや Umi Protocolのチームが強調していたアセットセントリックの部分で直感的にもこちらの方が分かりやすいですね。実際にオブジェクトを移動させているので!!

5. まとめ

Sui と Sui Moveについてまとめてみましたが、いかがでしたでしょうか?

実際にデジタルアセットをアカウントが所持するという説明を受けたときに最初は良く分からなかったのですが、実際にコードを書いてみて動かしてみてようやくその意味が分かり、Ethereumとの違いを認識することができました。

実際にアセットがオブジェクトとして存在し、本当にやり取りができるのでERC721などの設計に疑問を持っている開発者はSui MoveでDappを作る方が向いているかもしれませんね!

これまでEVM系のチェーンが一強だったイメージがありますが、Sui や XRPL、ICPなどの強力なブロックチェーンプロジェクトも目立ってきている印象です。2023年からはますますレイヤー1ブロックチェーンの戦いが激化しそうです!!

参考文献には学習や調査に役立ちそうなリンクをまとめていますのでぜひ参考にしてください!

ここまで読んでいただきありがとうございました!!

6. 参考文献

https://sui.io/

https://docs.google.com/presentation/d/1YmAivmbhsn_bC5SIEG4Osp7bMEhftJrb5h0f8SdRJi4/edit#slide=id.g24b3e7426c4_0_261

https://docs.google.com/presentation/d/1-pyR9_DUrFu6a2LXDo7JgFBLIGA0XAHUnPlaAveoYhA/edit#slide=id.g24b3e7426c4_0_261

https://github.com/umi-ag/suitter

https://docs.shinami.com/docs/sui-move-resources

https://www.youtube.com/watch?v=9QgFlmnHlNg

https://zenn.dev/noplan_inc/articles/1bf43864818b38

https://zenn.dev/pentaworld/articles/bbf1590f9c5ad2

https://github.com/MystenLabs/sui

https://zenn.dev/torohash/articles/910b925ff361be

https://coinpost.jp/?p=456931

https://coinpost.jp/?p=387756

https://suiecosystem.top/

https://arxiv.org/abs/2105.11827

https://qiita.com/maebaru/items/5b6cb981777624ab843c

https://docs.sui.io/build/move

https://blockchainjapan.hatenablog.jp/entry/2022/09/19/143318

https://suiexplorer.com/

https://github.com/mashharuki/GenerativeNFTDApp

Discussion