見出し画像

Azukiが発表したPBTのEIPを読んでみた

運良く某社(元?)CTOと2人で教えてもらう時間がゲットできたので、そこでの話を理解の整理としてまとめてみます。
タイトルがファルシのルシみたいになっちゃった。

TL;DR

・PBTは物理アイテムに埋め込んだチップをスキャンすることでのみ操作できるNFT
・ERC721をベースとしているが、TransferやsetApprovalForAllなどのメソッドが無効にされている
・NFTを誰かに送っても物理アイテムが手元にあればいつでも取り返せるため、あくまで物理アイテムの方がメインと言える

PBTとは

PBTはPhysical Backed Tokenの略で、物理アイテムに紐づけたNFTのことです。
先日Azukiが物理スケートボードの発表を行い、その中に含まれていたもので、EIPも申請しています。

日本ではStartbahnが取り組んでいるような領域です。

PBTが解決する課題

物理アイテムとNFTの紐づけは以前から事例などもあり、発想としては新しいものではありません。

しかし、物理アイテムとNFTの所有が必ずしも紐づかないという問題がありました。

PBTは物理アイテムにチップを埋め込み、それをNFTのTransfer時に必須とすることで、この問題を解決します。

Azukiの公式サイトを見てみる

・NFTと物理アイテムの所有の完全な同期
・Verificationをフルオンチェーンで行い、中央集権的なサーバーを用いない
ことがコンセプトとして挙げられています。

スケートボードには電子チップがついていて、これをスキャンすることで操作に必要な秘密鍵署名が得られるようになっているようです。
チップはKongという外部チームとの共同開発です。

物理アイテムが新しい所有者に渡った後、新しい所有者がチップをスキャンして自分のアドレスにNFTを移動させる、という処理になるようです。


EIPプロポーザルを見てみる

プロポーザルの内容は普通の英語でもまとめられています。
自分はエンジニアじゃないのでまずはそっちを見てみます。

・ERC721の拡張であること
・チップにECDSA secp256k1の鍵を保存できること
・チップから秘密鍵での署名ができること
・Account Bound Implementationが必要なこと
・NFTのMint時にチップのアドレスにマッピングされること
などが書かれています

また、チップ破損時やチップが物理アイテムから取り外されると動かなくなるよーということも書いてあります。当たり前ですが。

個人的に気になったのはここで、この時点では、NFTの操作権はコントラクトにあって、ScanしてMeta transactionするみたいなことを予想しています。

The approach also requires that the contract uses an account-bound implementation of [EIP-721](./eip-721.md) (where all [EIP-721](./eip-721.md) functions that transfer must throw, e.g. the "read only NFT registry" implementation referenced in [EIP-721](./eip-721.md)). This ensures that ownership of the physical item is required to initiate transfers and manage ownership of the NFT, through a new function introduced in this interface described below.


GitHubを見てみる

srcフォルダの中を見ると、3つのファイルとMocksというフォルダが入っているのがわかります。
3つのファイルの方がPBTの基礎コードで、Mocksの中身が実装例です。

画像1

まずERC721ReadOnlyの中身を見てみると、おなじみのOpenzeppelinのERC721を継承しつつ、ApproveやTransferに関わるメソッドがrevertで上書きされており、使えなくなっていることがわかります。

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

/**
* An implementation of 721 that's publicly readonly (no approvals or transfers exposed).
*/

contract ERC721ReadOnly is ERC721 {
   constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {}

   function approve(address to, uint256 tokenId) public virtual override {
       revert("ERC721 public approve not allowed");
   }

   function getApproved(uint256 tokenId) public view virtual override returns (address) {
       require(_exists(tokenId), "ERC721: invalid token ID");
       return address(0);
   }

   function setApprovalForAll(address operator, bool approved) public virtual override {
       revert("ERC721 public setApprovalForAll not allowed");
   }

   function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
       return false;
   }

   function transferFrom(address from, address to, uint256 tokenId) public virtual override {
       revert("ERC721 public transferFrom not allowed");
   }

   function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
       revert("ERC721 public safeTransferFrom not allowed");
   }

   function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
       revert("ERC721 public safeTransferFrom not allowed");
   }
}

次にPBTSimple.solの中身を見てみると、transferTokenWithChipというFunctionがあり、この中でチップの秘密鍵による署名が使われていることがわかります。

    function _transferTokenWithChip(
       bytes calldata signatureFromChip,
       uint256 blockNumberUsedInSig,
       bool useSafeTransferFrom
   ) internal virtual {
       uint256 tokenId = _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig).tokenId;
       if (useSafeTransferFrom) {
           _safeTransfer(ownerOf(tokenId), _msgSender(), tokenId, "");
       } else {
           _transfer(ownerOf(tokenId), _msgSender(), tokenId);
       }
   }

Mint時の処理はこのようになっていて、チップの署名をもとにTokenDataを読み出し、TokenIDを確認していることがわかります。
しかしMintしようとしているときに既にTokenDataが存在している・・・?

    function _mintTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig)
       internal
       returns (uint256)
   {
       TokenData memory tokenData = _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig);
       uint256 tokenId = tokenData.tokenId;
       _mint(_msgSender(), tokenId);
       emit PBTMint(tokenId, tokenData.chipAddress);
       return tokenId;
   }

同じファイル内に以下のような記載があり、
・TokenDataがTokenIDとチップアドレスと謎のBoolで構成されている
・チップアドレスとTokenDataが紐付けられている
ことがわかります。

struct TokenData {
       uint256 tokenId;
       address chipAddress;
       bool set;
   }

   /**
    * Mapping from chipAddress to TokenData
    */
   mapping(address => TokenData) _tokenDatas;

更に別に_seedChipToTokenMappingというものがありました。
どうやらNFTが既にMint済みかを確認し、まだであればMintしてTokenDataに(tokenid,chipAddress,true)を書き込むようです。
_tokenDatas[chipAddress]という記載があるので、{chipAddress,{tokenid,chipAddress,true}}みたいなデータがNFTごとに作られていくのかなと思います。
また謎のBoolはMint済みならTrueになるもので、要はMintフラグということもわかりました。

    // Should only be called for tokenIds that have not yet been minted
   // If the tokenId has already been minted, use _updateChips instead
   // TODO: consider preventing multiple chip addresses mapping to the same tokenId (store a tokenId->chip mapping)
   function _seedChipToTokenMapping(address[] memory chipAddresses, uint256[] memory tokenIds) internal {
       _seedChipToTokenMapping(chipAddresses, tokenIds, true);
   }

   function _seedChipToTokenMapping(
       address[] memory chipAddresses,
       uint256[] memory tokenIds,
       bool throwIfTokenAlreadyMinted
   ) internal {
       uint256 tokenIdsLength = tokenIds.length;
       if (tokenIdsLength != chipAddresses.length) {
           revert ArrayLengthMismatch();
       }
       for (uint256 i = 0; i < tokenIdsLength; ++i) {
           address chipAddress = chipAddresses[i];
           uint256 tokenId = tokenIds[i];
           if (throwIfTokenAlreadyMinted && _exists(tokenId)) {
               revert SeedingChipDataForExistingToken();
           }
           _tokenDatas[chipAddress] = TokenData(tokenId, chipAddress, true);
       }
   }

おそらく事業者側が物理アイテム作成時にseedChipToTokenMappingを実行して裏側でNFTを作っておき、事業者または物理アイテム購入者がmintTokenWithChipを呼び出すことで、NFTが移動するようという流れですかね。

また、_mintTokenWithChipの中に出てくる_mintは今回のソースコードの中には書かれていなかったので、おそらく大本のERC721から継承している、普通のMintメソッドを呼び出しているのかなと思います。


読んで思ったこと

送り先アドレスを自分で入力すればNFTだけ他の人に送ることもできそうですが、物理アイテムが手元にあればいつでも取り返せます。

これはNFTのレンタルに使えるかもしれません。NFTレンタルにおいてはいくつかアプローチがありますが、借りパクをどう防ぐかというのは1つの課題になります。
スマコンにロックするような方式もありえますが、これはブリッジ同様スマコンに資産が溜まってしまうので攻撃対象となります。
一方PBTのような仕組みであればロックなどをせずに直接送って直接取り返せるため、攻撃されづらいはずです。
また、貸している側が返却の強制力を完全に握っているのもポイントと言えます。

他にもハードウェアウォレットで応用できたりしないかなあと思ったりしました。
NFTNYCに行った際は襲われないか結構心配したものですが、このような形式のNFTが増えれば少し心配は減りそうです。
一方でNFT単体で取引は意味がないため、NFTらしさみたいなものは薄れるのかもしれません。遠くの人と交換するのが難しいため、地理的な距離感が重要になってきそうです。

この記事が気に入ったらサポートをしてみませんか?