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

Vyper ERC-721コントラクトウォークスルー

Vyper
ERC-721
Python
初級
Ori Pomerantz
2021年4月1日
35 分の読書

はじめに

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

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

コントラクト

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

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

from vyper.interfaces import ERC721

implements: ERC721

ERC-721インターフェイスはVyper言語に組み込まれています。 コードの定義はこちらで確認できます (opens in a new tab)。 インターフェイスの定義はVyperではなくPythonで書かれています。なぜなら、インターフェイスはブロックチェーン内で使用されるだけでなく、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ビットです。 通常、トークンIDはトークンが表すものの説明を ハッシュ化して作成されます。

            _data: Bytes[1024]

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

        ) -> bytes32: view

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

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

イベント

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

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

ERC-721の承認は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]

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

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

この変数は、各所有者のトークン数を保持します。 所有者からトークンへのマッピングはないため、 特定の所有者が所有するトークンを識別する唯一の方法は、ブロックチェーンのイベント履歴を遡り、 適切なTransferイベントを見ることです。 この変数を使用して、すべてのNFTをいつ取得したかを知ることができ、 それ以上調べる必要がありません。

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

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

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

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

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

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

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

# @dev ERC721のERC165インターフェイス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.<変数名>を使用します(これもPythonと同じです)。

ビュー関数

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

@view
@external

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

  • @viewは、この関数がビューであることを指定します。
  • @externalは、この特定の関数がトランザクションや他のコントラクトによって呼び出されることを指定します。
def supportsInterface(_interfaceID: bytes32) -> bool:

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

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

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

### ビュー関数 ###

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

この行は、_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. アドレスがトークンの所有者のオペレーターである

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

送金に問題がある場合、呼び出しを元に戻します。

必要な場合のみ、値を変更してください。 状態変数はストレージに存在します。 ストレージへの書き込みは、 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コントラクトを作成したアカウント)のみが新しいトークンをミントできます。 これは、将来ミンターの IDを変更したい場合に問題になる可能性があります。 本番環境のコントラクトでは、ミンターが ミンター権限を他の誰かに譲渡できる関数が必要になるでしょう。

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

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

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

このコントラクトの使用

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

結論

復習のために、このコントラクトの最も重要なアイデアをいくつか紹介します。

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

では、セキュアなVyperコントラクトを実装してみましょう。

私の他の作品はこちらでご覧いただけます (opens in a new tab).

ページの最終更新: 2026年4月28日

このチュートリアルは役に立ちましたか?