JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

ERC721に準拠したトークンをフルスクラッチ実装する

ERC721は価値の交換が不可能なトークン。詳細は他の記事で。
NFT(Non Fungible Token)という略称を使います。

EIPはこちら。最近(一月前くらい?)StatusがFinalになったようです! EIPs/eip-721.md at master · ethereum/EIPs · GitHub

今回も学習のため自分で実装していきますが、実際に稼働するものを作るときはセキュリティ上の理由からOpenZeppelinを使って作成することをおすすめします。

また、ERC721はERC165のサブセットですので、ERC165のメソッドも実装しなければなりません。
実装するべきメソッドはERC20より多いですね・・1つずつ見ていきます。

準拠すべきメソッド・イベント

イベント

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

NFTの所有者が変わったときに発火するイベント。
NFTが新規に生成された時や、破壊された時にも発火します。
ただし、コントラクトをデプロイするタイミングでNFTを生成する時は呼び出す必要はありません。

event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

approveが成功した時に発火するイベント。
NFTの転送が成功すると、approveはfalseに変わります

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

全てのNFTを操作可能な権限のapproveが成功した時に発火するイベント。

メソッド

以下はERC721のインターフェース

function balanceOf(address _owner) external view returns (uint256);

NFTの所持数を返します

function ownerOf(uint256 _tokenId) external view returns (address);

NFTの所有者のアドレスを返します

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

NFTの所有権をfromからtoに変更します。
msg.senderがNFTの所有者、認証されたオペレータ、このNFTの承認済みアドレスでない場合はthrowされます。
fromがNFTの所有者でない場合、toがzeroアドレスである場合、_tokenIdが存在しない場合の時にもthrowされます。

変更が完了すると、toがスマートコントラクト(コードサイズ > 0)かどうかをチェックします。
コントラクトであれば、
toで onERC721Receivedを呼び出し、戻り値がbytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))でなければスローします。

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

先のsafeTransferFromの第三引数であるdataを””に設定して呼び出します

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

NFTの所有権をfromからtoに変更します。
呼び出し側は、NFTを受信できるかどうか、またはそれらが永久に失われる可能性があることを確認する責任があります。
msg.senderがNFTの所有者、認証されたオペレータ、このNFTの承認済みアドレスでない場合はthrowされます。
fromがNFTの所有者でない場合、toがzeroアドレス、_tokenIdが存在しない値あってもthrowされます。

function approve(address _approved, uint256 _tokenId) external payable;

_approvedのアドレスがtokenを移動することを許可します
msg.senderがNFTの所有者、認証されたオペレータでない場合はthrowされます。

function setApprovalForAll(address _operator, bool _approved) external;

オペレータが msg.senderのすべてのNFTを管理する事を許可する。もしくは許可を取り消す。
ApprovalForAllイベントを発火します。コントラクトは複数のオペレータを持てるような構造にしなければなりません。

function getApproved(uint256 _tokenId) external view returns (address);

approveしているアドレスを取得します。承認しているアドレスがない場合はzeroアドレスを返します

function isApprovedForAll(address _owner, address _operator) external view returns (bool);

指定したアドレスがオペレータとして許可されているかを確認します

以下はERC165のインターフェース
EIPs/eip-165.md at master · ethereum/EIPs · GitHub

ERC165は、スマートコントラクトがどのインターフェースを実装しているか、を公開するメソッドを定義しています

function supportsInterface(bytes4 interfaceID) external view returns (bool);

interfaceIDで指定されたinterfaceが実装されているかを確認します。

interfaceIDは、interfaceに含まれるすべての関数セレクタのXORとして定義されます。

関数セレクタの詳細についてはApplication Binary Interface Specification — Solidity 0.4.24 documentation をご確認下さい。

ERC165にあるサンプルコードをそのまま抜粋してきました。
return i.hello.selector ^ i.world.selector;でinterfaceIDを計算しています。

publicかexternalのアクセス修飾子がついた関数には、特別なメソッドとして「selector」が付与されます。
*method名が同じで引数が違うものがある場合は、selectorは使えません。なので、bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)”)と直接文字列を指定します

pragma solidity ^0.4.20;

interface Solidity101 {
    function hello() external pure;
    function world(int) external pure;
}

contract Selector {
    function calculateSelector() public pure returns (bytes4) {
        Solidity101 i;
        return i.hello.selector ^ i.world.selector;
    }
}

以上が実装必須のメソッドです。
トークンを新規作成する処理(minting)とトークンを破棄する処理(burning)は要件に含まれていません。
必要に応じて独自で実装するようになっています。

また、wallet/broker/auctionなどのアプリケーションで、safeTransferFromを呼び出す際は、

function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); 

のcallを受け入れられるようにしておかないといけないことに留意して下さい。

Optionalなメソッドもいくつかあります。

/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    /// @notice A descriptive name for a collection of NFTs in this contract
    function name() external view returns (string _name);

    /// @notice An abbreviated name for NFTs in this contract
    function symbol() external view returns (string _symbol);

    /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
    /// @dev Throws if `_tokenId` is not a valid NFT. URIs are defined in RFC
    ///  3986. The URI may point to a JSON file that conforms to the "ERC721
    ///  Metadata JSON Schema".
    function tokenURI(uint256 _tokenId) external view returns (string);
}
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice Count NFTs tracked by this contract
    /// @return A count of valid NFTs tracked by this contract, where each one of
    ///  them has an assigned and queryable owner not equal to the zero address
    function totalSupply() external view returns (uint256);

    /// @notice Enumerate valid NFTs
    /// @dev Throws if `_index` >= `totalSupply()`.
    /// @param _index A counter less than `totalSupply()`
    /// @return The token identifier for the `_index`th NFT,
    ///  (sort order not specified)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice Enumerate NFTs assigned to an owner
    /// @dev Throws if `_index` >= `balanceOf(_owner)` or if
    ///  `_owner` is the zero address, representing invalid NFTs.
    /// @param _owner An address where we are interested in NFTs owned by them
    /// @param _index A counter less than `balanceOf(_owner)`
    /// @return The token identifier for the `_index`th NFT assigned to `_owner`,
    ///   (sort order not specified)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

実装する

コードが長くなるので、GitHub - 1234224576/ERC721TokenSample: ERC721に準拠したトークンのサンプル を参照ください・・。
ユニットテストも記述していますので、何かの参考になれば幸いです。

不十分な点が数多く存在すると思われますので、issue/PRを投げてもらえると助かります!

デプロイ

Ropstenテストネットワークに初期バージョンをデプロイしました。
SNFT: 0x4e5590c877f07e86af879bcb7a03b440d57607c4