メインコンテンツへスキップ

Vyper ERC-721コントラクトの解説

Vyper
erc-721
Python
初級
オリ・ポメランツ
2021年4月1日
36 分で読めます

はじめに

ERC-721標準は、非代替性トークン(NFT)の所有権を保持するために使用されます。 個々のトークン間に違いがないため、ERC-20トークンはコモディティのように振る舞います。 対照的に、ERC-721トークンは、異なる猫のキャラクター (opens in a new tab)や異なる不動産の権利書など、類似しているが同一ではない資産のために設計されています。

この記事では、Ryuya NakamuraのERC-721コントラクト (opens in a new tab)を分析します。 このコントラクトは、Solidityよりも安全でないコードを書きにくくするように設計されたPython風のコントラクト言語であるVyper (opens in a new tab)で書かれています。

コントラクト

# @dev ERC-721非代替性トークン標準の実装。
# @author Ryuya Nakamura (@nrryuya)
# 変更元: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Vyperのコメントは、Pythonと同様にハッシュ(#)で始まり、行末まで続きます。@<keyword>を含むコメントは、人間が読めるドキュメントを生成するためにNatSpec (opens in a new tab)によって使用されます。

from vyper.interfaces import ERC721

implements: ERC721

ERC-721インターフェースはVyper言語に組み込まれています。 コードの定義はこちらで確認できます (opens in a new tab)。 インターフェースはブロックチェーン内だけでなく、Pythonで書かれている可能性のある外部クライアントからブロックチェーンにトランザクションを送信する際にも使用されるため、インターフェース定義はVyperではなくPythonで書かれています。

最初の行はインターフェースをインポートし、2行目はここでそれを実装することを指定しています。

ERC721Receiverインターフェース

# safeTransferFrom()によって呼び出されるコントラクトのインターフェース
interface ERC721Receiver:
    def onERC721Received(

ERC-721は2種類の送金をサポートしています:

  • transferFrom:送信者が任意の宛先アドレスを指定でき、送金の責任を送信者に負わせます。これは、無効なアドレスに送金できることを意味し、その場合NFTは永久に失われます。
  • safeTransferFrom:宛先アドレスがコントラクトであるかを確認します。コントラクトである場合、ERC-721コントラクトは受信側コントラクトにNFTを受け取るかどうかを尋ねます。

safeTransferFromのリクエストに応答するには、受信側コントラクトはERC721Receiverを実装する必要があります。

            _operator: address,
            _from: address,

_fromアドレスはトークンの現在の所有者です。_operatorアドレスは送金をリクエストしたアドレスです(アローワンスがあるため、これら2つは同じではない場合があります)。

            _tokenId: uint256,

ERC-721のトークンIDは256ビットです。通常、これらはトークンが表すものの説明をハッシュ化することによって作成されます。

            _data: Bytes[1024]

リクエストには最大1024バイトのユーザーデータを含めることができます。

        ) -> bytes32: view

コントラクトが誤って送金を受け入れるケースを防ぐため、戻り値はブール値ではなく、特定の値を持つ256ビットです。

この関数はviewであり、ブロックチェーンの状態を読み取ることはできますが、変更することはできないことを意味します。

イベント

イベントは、ブロックチェーン外のユーザーやサーバーにイベントを通知するために発行されます。イベントの内容はブロックチェーン上のコントラクトからは利用できないことに注意してください。

これはERC-20のTransferイベントに似ていますが、金額の代わりにtokenIdを報告する点が異なります。ゼロ・アドレスを所有する人はいないため、慣例としてトークンの作成と破棄を報告するために使用します。

ERC-721の承認(approval)は、ERC-20のアローワンスに似ています。特定のアドレスが特定のトークンを送金することが許可されます。これにより、コントラクトがトークンを受け取ったときに応答するメカニズムが提供されます。コントラクトはイベントをリッスンできないため、単にトークンを送金しただけでは、コントラクトはそれを「知る」ことができません。この方法では、所有者が最初に承認を提出し、次にコントラクトにリクエストを送信します。「トークンXの送金を承認したので、...を実行してください」。

これは、ERC-721標準をERC-20標準に似せるための設計上の選択です。ERC-721トークンは代替不可能であるため、コントラクトはトークンの所有権を確認することで、特定のトークンを受け取ったことを識別することもできます。

委任状のように、特定の種類(特定のコントラクトによって管理されるもの)のアカウントのすべてのトークンを管理できる_オペレーター_を持つことが便利な場合があります。たとえば、私が6か月間連絡を取っていないかを確認し、そうであれば私の資産を相続人に分配するコントラクトにそのような権限を与えたいと思うかもしれません(相続人の一人がそれを要求した場合。コントラクトはトランザクションによって呼び出されない限り何もできません)。ERC-20では、相続コントラクトに高いアローワンスを与えるだけで済みますが、ERC-721ではトークンが代替不可能であるため、それは機能しません。これはそれに相当するものです。

approvedの値は、イベントが承認のためのものか、承認の取り消しのためのものかを示します。

状態変数

これらの変数は、どのトークンが利用可能で誰が所有しているかという、トークンの現在の状態を含んでいます。これらのほとんどはHashMapオブジェクトであり、2つの型間に存在する単方向のマッピング (opens in a new tab)です。

# @dev NFT IDからそれを所有するアドレスへのマッピング。
idToOwner: HashMap[uint256, address]

# @dev NFT IDから承認済みアドレスへのマッピング。
idToApprovals: HashMap[uint256, address]

イーサリアムにおけるユーザーとコントラクトのアイデンティティは、160ビットのアドレスで表されます。これら2つの変数は、トークンIDからその所有者および送金を承認された者(それぞれ最大1人)にマッピングします。イーサリアムでは、初期化されていないデータは常にゼロであるため、所有者や承認された送金者がいない場合、そのトークンの値はゼロになります。

# @dev 所有者のアドレスからそのトークンの数へのマッピング。
ownerToNFTokenCount: HashMap[address, uint256]

この変数は、各所有者のトークン数を保持します。所有者からトークンへのマッピングはないため、特定の所有者が所有するトークンを特定する唯一の方法は、ブロックチェーンのイベント履歴を遡り、該当するTransferイベントを確認することです。この変数を使用することで、すべてのNFTを取得し終え、これ以上過去を遡る必要がないタイミングを知ることができます。

このアルゴリズムは、ユーザーインターフェースと外部サーバーでのみ機能することに注意してください。ブロックチェーン上で実行されているコード自体は、過去のイベントを読み取ることはできません。

# @dev 所有者のアドレスからオペレーターのアドレスのマッピングへのマッピング。
ownerToOperators: HashMap[address, HashMap[address, bool]]

アカウントは複数のオペレーターを持つ場合があります。各キーが単一の値につながるため、単純なHashMapではそれらを追跡するのに不十分です。代わりに、値としてHashMap[address, bool]を使用できます。デフォルトでは、各アドレスの値はFalseであり、これはオペレーターではないことを意味します。必要に応じて値をTrueに設定できます。

# @dev トークンをミントできるミンターのアドレス
minter: address

新しいトークンは何らかの方法で作成される必要があります。このコントラクトでは、それを行うことが許可されている単一のエンティティ、minterが存在します。これは、たとえばゲームには十分である可能性が高いです。他の目的では、より複雑なビジネスロジックを作成する必要があるかもしれません。

# @dev インターフェースIDからそれがサポートされているかどうかのboolへのマッピング
supportedInterfaces: HashMap[bytes32, bool]

# @dev ERC-165のERC-165インターフェースID
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

# @dev ERC-721のERC-165インターフェースID
ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

ERC-165 (opens in a new tab)は、コントラクトがどのERCに準拠しているか、アプリケーションがどのように通信できるかを開示するメカニズムを指定しています。この場合、コントラクトはERC-165とERC-721に準拠しています。

関数

これらは実際にERC-721を実装する関数です。

コンストラクタ

@external
def __init__():

Vyperでは、Pythonと同様に、コンストラクタ関数は__init__と呼ばれます。

    """
    @dev コントラクトのコンストラクタ。
    """

PythonおよびVyperでは、複数行の文字列("""で始まり終わる)を指定し、それを一切使用しないことでコメントを作成することもできます。これらのコメントにはNatSpec (opens in a new tab)を含めることもできます。

    self.supportedInterfaces[ERC165_INTERFACE_ID] = True
    self.supportedInterfaces[ERC721_INTERFACE_ID] = True
    self.minter = msg.sender

状態変数にアクセスするには、self.<variable name>を使用します(これもPythonと同じです)。

View関数

これらはブロックチェーンの状態を変更しない関数であり、外部から呼び出された場合は無料で実行できます。View関数がコントラクトから呼び出された場合、依然としてすべてのノードで実行される必要があるため、ガスがかかります。

@view
@external

関数定義の前にあり、アットマーク(@)で始まるこれらのキーワードは_デコレーション_と呼ばれます。これらは関数を呼び出すことができる状況を指定します。

  • @viewは、この関数がViewであることを指定します。
  • @externalは、この特定の関数がトランザクションや他のコントラクトから呼び出せることを指定します。
def supportsInterface(_interfaceID: bytes32) -> bool:

Pythonとは対照的に、Vyperは静的型付け言語 (opens in a new tab)です。データ型 (opens in a new tab)を特定せずに変数や関数パラメータを宣言することはできません。この場合、入力パラメータは256ビットの値であるbytes32です(256ビットはイーサリアム仮想マシンのネイティブワードサイズです)。出力はブール値です。慣例として、関数パラメータの名前はアンダースコア(_)で始まります。

    """
    @dev インターフェースの識別はERC-165で指定されています。
    @param _interfaceID インターフェースのID
    """
    return self.supportedInterfaces[_interfaceID]

コンストラクタ(__init__)で設定されたself.supportedInterfaces HashMapから値を返します。

### VIEW関数 ###

これらは、トークンに関する情報をユーザーや他のコントラクトが利用できるようにするView関数です。

この行は、_ownerがゼロでないことをアサート (opens in a new tab)します。ゼロの場合、エラーとなり操作はリバートされます。

イーサリアム仮想マシン(EVM)では、値が保存されていないストレージはすべてゼロになります。_tokenIdにトークンがない場合、self.idToOwner[_tokenId]の値はゼロになります。その場合、関数はリバートされます。

getApprovedはゼロを返す_可能性がある_ことに注意してください。トークンが有効な場合、self.idToApprovals[_tokenId]を返します。承認者がいない場合、その値はゼロになります。

この関数は、_operatorがこのコントラクト内の_ownerのすべてのトークンを管理することを許可されているかを確認します。複数のオペレーターが存在する可能性があるため、これは2レベルのHashMapになっています。

送金ヘルパー関数

これらの関数は、トークンの送金や管理の一部となる操作を実装します。


### 送金関数のヘルパー ###

@view
@internal

このデコレーション@internalは、関数が同じコントラクト内の他の関数からのみアクセス可能であることを意味します。慣例として、これらの関数名もアンダースコア(_)で始まります。

アドレスがトークンの送金を許可されるには、3つの方法があります:

  1. アドレスがトークンの所有者である
  2. アドレスがそのトークンを使用することを承認されている
  3. アドレスがトークン所有者のオペレーターである

上記の関数は状態を変更しないため、Viewにすることができます。運用コストを削減するために、Viewに_できる_関数はすべてViewに_すべき_です。

送金に問題がある場合、呼び出しをリバートします。

必要な場合のみ値を変更します。状態変数はストレージに存在します。ストレージへの書き込みは、EVM(イーサリアム仮想マシン)が行う最も高価な操作の1つです(ガスの観点から)。したがって、それを最小限に抑えることは良い考えであり、既存の値を書き込むだけでも高いコストがかかります。

トークンを送金する方法は2つ(通常と安全)ありますが、監査を容易にするためにコード内で送金を行う場所を1か所だけにしたいので、この内部関数を用意しています。

Vyperでイベントを発行するには、logステートメントを使用します(詳細はこちらを参照してください (opens in a new tab))。

送金関数

この関数を使用すると、任意のアドレスに送金できます。アドレスがユーザーであるか、トークンの送金方法を知っているコントラクトでない限り、送金したトークンはそのアドレスにスタックし、役に立たなくなります。

問題があればいずれにせよリバートし、呼び出しで行われたすべてがキャンセルされるため、最初に送金を行っても問題ありません。

    if _to.is_contract: # `_to`がコントラクトのアドレスであるかを確認します

まず、アドレスがコントラクトであるか(コードを持っているか)を確認します。そうでない場合は、ユーザーアドレスであると想定し、ユーザーがトークンを使用または送金できると考えます。しかし、それで安心しきってはいけません。safeTransferFromを使用しても、誰も秘密鍵を知らないアドレスに送金すると、トークンを失う可能性があります。

        returnValue: bytes32 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)

ターゲットコントラクトを呼び出して、ERC-721トークンを受け取ることができるかを確認します。

        # 送金先が'onERC721Received'を実装していないコントラクトの場合はスローします
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

宛先がコントラクトであっても、ERC-721トークンを受け入れない(またはこの特定の送金を受け入れないと決定した)場合は、リバートします。

慣例として、承認者を置きたくない場合は、自分自身ではなくゼロ・アドレスを指定します。

    # 要件を確認します
    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
    assert (senderIsOwner or senderIsApprovedForAll)

承認を設定するには、所有者であるか、所有者によって承認されたオペレーターである必要があります。

新しいトークンのミントと既存のトークンの破棄

コントラクトを作成したアカウントはminterであり、新しいNFTをミントする権限を持つスーパーユーザーです。しかし、そのアカウントでさえ既存のトークンをバーンすることは許可されていません。所有者、または所有者によって承認されたエンティティのみがそれを行うことができます。

### ミントおよびバーン関数 ###

@external
def mint(_to: address, _tokenId: uint256) -> bool:

操作が失敗した場合はリバートされるため、この関数は常にTrueを返します。

ミンター(ERC-721コントラクトを作成したアカウント)のみが新しいトークンをミントできます。将来、ミンターのアイデンティティを変更したい場合、これが問題になる可能性があります。本番環境のコントラクトでは、ミンターがミンター権限を他の誰かに譲渡できる関数が必要になるでしょう。

    # `_to`がゼロ・アドレスの場合はスローします
    assert _to != ZERO_ADDRESS
    # NFTを追加します。`_tokenId`が誰かに所有されている場合はスローします
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

慣例として、新しいトークンのミンティングはゼロ・アドレスからの送金としてカウントされます。

トークンの送金を許可されている人は誰でも、それをバーンすることが許可されています。バーンはゼロ・アドレスへの送金と同等に見えますが、ゼロ・アドレスは実際にはトークンを受け取りません。これにより、トークンに使用されていたすべてのストレージを解放でき、トランザクションのガスコストを削減できます。

このコントラクトの使用

Solidityとは対照的に、Vyperには継承がありません。これは、コードをより明確にし、結果として安全性を確保しやすくするための意図的な設計上の選択です。したがって、独自のVyper ERC-721コントラクトを作成するには、このコントラクト (opens in a new tab)を取得し、必要なビジネスロジックを実装するように変更します。

まとめ

復習として、このコントラクトにおける最も重要なアイデアのいくつかを以下に示します:

  • 安全な送金でERC-721トークンを受け取るには、コントラクトはERC721Receiverインターフェースを実装する必要があります。
  • 安全な送金を使用しても、秘密鍵が不明なアドレスに送信すると、トークンがスタックする可能性があります。
  • 操作に問題がある場合、単に失敗の値を返すのではなく、呼び出しをrevert(リバート)することをお勧めします。
  • ERC-721トークンは、所有者がいる場合に存在します。
  • NFTの送金を承認されるには3つの方法があります。所有者であるか、特定のトークンに対して承認されているか、所有者のすべてのトークンのオペレーターであるかのいずれかです。
  • 過去のイベントはブロックチェーンの外部でのみ表示されます。ブロックチェーン内で実行されているコードはそれらを表示できません。

さあ、安全なVyperコントラクトを実装しましょう。

私の他の作品はこちらをご覧ください (opens in a new tab)