Help us understand the problem. What is going on with this article?

OpenZeppelinのERC721が更新されたので触ってみる

More than 1 year has passed since last update.

概要

OpenZeppelinのERC721が更新されたので見てみる
https://qiita.com/marutake/items/671719864e1fbbc757f1

の続き。せっかくなので練習がてら何か作ってみる。

やること

  • 取引所で扱うようなトークンではなく、ゲームに転用できるようなトークンを作る(crypto kittiesっぽいの)
    • むしろ取引所では使えない(safeTransferをブロックしてるので)
  • トレード的な売買のメソッドだけ作る
  • トークン生成と更新ができるようにする
  • OpenZeppelinのソースはいじらず、追加のみ

要件決め

トークンの扱い

生成時は空っぽのトークンを生成する。

updateでトークンのmetaに情報を書き込めるようにする

トークンの販売(トレード)

トークン所有者Aは購入者Bに売買を持ちかける (approve)

購入者Bは、金額に納得したら購入する (buy → safeTransferFrom)

Aが金額を受け取る(withdraw)

環境

  • 昔はgethにsolcあててコンパイルからの動作確認ができたが、現バージョンではサポートしていない。
  • truffleは開発しやすく、OpenZeppelinのプラグインもあるため便利だが、private_netに移すときちょいちょいめんどい
    • gethとtruffleでは使用しているweb3のバージョンが異なる & gethはweb3をバイナリでハードコードしているので、変更が面倒

ので、今回はbrowser-solidityでやる。

mac  os 10.12.4
geth 1.8.2-stable 
browser-solidity(remix-ide)

gethのインストールはリンク先参照

インストール(browser-solidity)

バージョンが古いと動きが怪しいので、後継のremix-ideをもってくる。

git clone https://github.com/ethereum/remix-ide.git
cd remix-ide
npm install
npm run build && npm run serve
npm start

起動

http://localhost:8080/

スクリーンショット 2018-03-27 18.47.52.png

画面右上のタブから Run → Environment → Web3 Providerを選択。
geth起動時にrpcdomainで指定したurlを入力して接続
スクリーンショット 2018-03-27 18.47.12.png

必要なファイルのインポート (4/2 不要でした)

githubのパスを指定して必要なコントラクトをimportし実装する

OpenZeppelinをもってくる

適当にターミナル開いて

cd ~
mkdir hoge
cd hoge

git clone https://github.com/OpenZeppelin/zeppelin-solidity.git

で、ターミナル閉じる。

browser-solidityに戻って、画面左上のとこからフォルダを選択

スクリーンショット 2018-03-27 18.54.13.png

で、必要なファイルを開く

スクリーンショット 2018-03-27 18.56.52.png

必要なファイル

  • AddressUtils.sol
  • DeprecatedERC721.sol
  • ERC721.sol
  • ERC721Basic.sol
  • ERC721BasicToken.sol
  • ERC721Holder.sol
  • ERC721Receiver.sol
  • ERC721Token.sol
  • ERC721TokenMock.sol
  • Ownable.sol
  • PullPayment.sol
  • SafeMath.sol

  • ERC721MaruToken.sol

※ ERC721MaruToken.solは自作なので、+のアイコンを選択して好きな名前で作る。

importパスの修正

browser-solidityで開いたファイルは、元位置のファイルを開くのではなく、browser-solidity上にコピーを生成する。
ので、importの参照を全部 ./ファイル名 に修正する。

※ githubのパス指定でもいけるて記事をどこかで見たが、自分の環境ではできなかったので諦めた。
(4/2 追記)
githubのパス指定でいけました

全ファイルのimportを修正したら、画面右上タブのcompileを選択。
セレクトボックスからERC721Token.solを選択し、start to compileを押す。
選択肢に出てこなかったら適当なファイルをコンパイルしてるとそのうち出てくると思う。

スクリーンショット 2018-03-27 19.06.22.png

エラーがある場合はピンクのラベルでエラーと出てくるので、なくなるまで修正する。

ここまで下準備

ここから製造

実装

  • ERC721Tokenを継承して作る。
  • 購入のあれそれが面倒なのでPullPaymentsを継承
  • ついでにOwnableも継承する
ERC721MaruToken.sol
pragma solidity ^0.4.18;

//import "./ERC721Token.sol";
//import "./Ownable.sol";
//import "./PullPayment.sol";

import "github.com/OpenZeppelin/zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";
import "github.com/OpenZeppelin/zeppelin-solidity/contracts/ownership/Ownable.sol";
import "github.com/OpenZeppelin/zeppelin-solidity/contracts/payment/PullPayment.sol";

/**
 * @title ERC721MaruToken
 */
contract ERC721MaruToken is ERC721Token, Ownable, PullPayment {
    uint private milli = 16;
    uint private milliEth = 10 ** milli;

    // token struct
    struct Maru {
        string  name;
        uint16  maruType;
        uint16  sold;
        uint32  readyTime;
        uint256 price;
    }
    Maru[]  private marus;
    uint256 private price = 1 ether;

    function ERC721MaruToken(string name, string symbol) public ERC721Token(name, symbol) {}

    function mint() payable public {
        require(price == msg.value);
        require(owner != address(0));
        asyncSend(owner, price);
        uint256 id = marus.push(Maru('plane', 0, 0, 0, 0)) - 1;
        super._mint(msg.sender, id);
    }

    // トークンの属性更新
    function updateMaru(uint256 _tokenId, string _name, uint16 _maruType) external{
        require(ownerOf(_tokenId) == msg.sender);
        marus[_tokenId].name = _name;
        marus[_tokenId].maruType = _maruType;
        marus[_tokenId].readyTime = uint32(now);
    }

    // 承認されたトークンを購入(個人間)
    function buyMaru(uint256 _tokenId) payable public
    {
        address seller = ownerOf(_tokenId);
        require(seller != address(0));
        require(seller != address(this));
        require(marus[_tokenId].price == msg.value);
        require(msg.sender == getApproved(_tokenId));

        marus[_tokenId].sold = 1;
        asyncSend(seller, msg.value);
        safeTransferFrom(seller, msg.sender, _tokenId);
    }

    function getMaru(uint256 _tokenId) public view returns (address, string, uint16, uint16, uint32, uint256, address) {
        return  (ownerOf(_tokenId), marus[_tokenId].name, marus[_tokenId].maruType,  marus[_tokenId].sold, marus[_tokenId].readyTime, marus[_tokenId].price, getApproved(_tokenId));
    }

    function tokensOf(address _owner) public view returns (uint256[]) {
        return ownedTokens[_owner];
    }

    // 運営からトークンを購入する時の金額
    function setPrice(uint256 _milliEth) external onlyOwner {
        price = _milliEth * milliEth;
    }

    function getPrice() external view returns(uint256) {
        return price;
    }

    // 個人売買の時の価格
    function setMaruPrice(uint256 _tokenId, uint256 _milliEth) external{
        require(ownerOf(_tokenId) == msg.sender);
        marus[_tokenId].price = _milliEth * milliEth;
    }

    function approve(address _to, uint256 _tokenId) public {
        require(marus[_tokenId].sold == 0);
        super.approve(_to, _tokenId);
    }

    function safeTransferFrom(address _from, address _to, uint256 _tokenId) public canTransfer(_tokenId) {
        require(marus[_tokenId].sold == 1);
        super.safeTransferFrom(_from, _to, _tokenId, "");
    }

    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes _data) public canTransfer(_tokenId) {
        require(marus[_tokenId].sold == 1);
        super.safeTransferFrom(_from, _to, _tokenId, _data);
    }

    function burn(uint256 _tokenId) public {
        super._burn(ownerOf(_tokenId), _tokenId);
        if (marus.length != 0) {
            delete marus[_tokenId];
        }
    }

    function setTokenURI(uint256 _tokenId, string _uri) public {
        super._setTokenURI(_tokenId, _uri);
    }

    function() payable public{
        mint();
    }
}

解説

コンストラクタ

名称とシンボルを設定して作成。

    function ERC721MaruToken(string name, string symbol) public ERC721Token(name, symbol) {}

ユニークトークンの実体

ERC721Metaが存在しているが、コレジャナイ感が強いので今回は自前で持つ。

    // token struct
    struct Maru {
        string  name;
        uint16  maruType;
        uint16  sold;
        uint32  readyTime;
        uint256 price;
    }
    Maru[]  private marus;

属性は適当に作っている。処理に関係するのは今回この2つ

sold : トレードで購入者が支払い済みなら1、そうでなければ0。これが1の時しかトークンを受け取れない。
              0の時でないと所有者はapproveを変更できない。
price: 販売金額。設定する時は1/100eth単位で指定する

購入(運営から)

トークンの販売価格と同額のethが送られてきた時にトークンを発行する。
ethはコントラクトが保管し、オーナーのアドレスに蓄積される。
withDrawで引き出せる。

金額はsetPriceで変更できるが、変更できるユーザは運営のみ。

    function mint() payable public {
        require(price == msg.value);
        require(owner != address(0));
        asyncSend(owner, price);
        uint256 id = marus.push(Maru('plane', 0, 0, 0, 0)) - 1;
        super._mint(msg.sender, id);
    }

トークン更新

metaの情報を書き込む。
ゲームなどで使う場合、これがガチャだったりレベルアップだったりと数が増える。
今回は所有者が好きに書き込める体になっているが、ownerOnlyにしてサービス経由で運営が書き込むようになるはず。
書き込みはGasがかかるので注意。

    // トークンの属性更新
    function updateMaru(uint256 _tokenId, string _name, uint16 _maruType) external{
        require(ownerOf(_tokenId) == msg.sender);
        marus[_tokenId].name = _name;
        marus[_tokenId].maruType = _maruType;
        marus[_tokenId].readyTime = uint32(now);
    }

購入(トレード)

所有者がsetMaruPriceで金額を決め、approveで売りたい人を指定する。
購入者が設定されている金額を払うと所有権が購入者に移る。
払った金額はコントラクトに蓄積されており、所有者がwithDrawを叩くとethを得る事ができる。

    // 個人売買の時の価格
    function setMaruPrice(uint256 _tokenId, uint256 _milliEth) external{
        require(ownerOf(_tokenId) == msg.sender);
        marus[_tokenId].price = _milliEth * milliEth;
    }

    // 承認
    function approve(address _to, uint256 _tokenId) public {
        require(marus[_tokenId].sold == 0);
        super.approve(_to, _tokenId);
    }

    // 承認されたトークンを購入(個人間)
    function buyMaru(uint256 _tokenId) payable public
    {
        address seller = ownerOf(_tokenId);
        require(seller != address(0));
        require(seller != address(this));
        require(marus[_tokenId].price == msg.value);
        require(msg.sender == getApproved(_tokenId));

        marus[_tokenId].sold = 1;
        asyncSend(seller, msg.value);
        safeTransferFrom(seller, msg.sender, _tokenId);
    }

buyMaruの中にsafeTransferFromを入れているが、本来ここに入れないでsoldだけを立て、その後safeTransferFromを改めて叩く方がいいんでないかなと思ってみたり。
approveがあるのに販売の進行ををsoldで管理しているのは、safeTransferFromがpublicなので金払わずに抜けちゃうため。

自分が勘違いしてるか見落としてる可能性もあるが、今回はとりあえずこんな感じ。

safeTransferFromのブロック

現状だと承認されているトークンを無料で奪えるため、購入フラグが立っていなければ失敗にする。

    function safeTransferFrom(address _from, address _to, uint256 _tokenId) public canTransfer(_tokenId) {
        require(marus[_tokenId].sold == 1);
        super.safeTransferFrom(_from, _to, _tokenId, "");
    }

    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes _data) public canTransfer(_tokenId) {
        require(marus[_tokenId].sold == 1);
        super.safeTransferFrom(_from, _to, _tokenId, _data);
    }

fallback

コントラクトに送金した時、空トークンを発行する。
Mintと同じ動きでいいと思うけどどうなんだろう。

    function() payable public{
        mint();
    }

デプロイ

HogeTokenを作ったらStart to compile

スクリーンショット 2018-03-27 19.18.56.png

コンパイルが成功したら、右上タブから「Run」選択
スクリーンショット 2018-03-27 19.21.55.png

真ん中のセレクトボックスで自分の作ったHogeTokenが増えてるはずなので選択。
無ければコンパイルに失敗してる。

「Create」ボタンの左側が入力できるようになっているので、引数を指定してCreateをポチっと。

例:
"トークン名", "シンボル名"

画面下のコンソールに「creation of HOGEToken pending...」と表示されるので、gethで

miner.start()

#チョット待ってから
miner.stop()

リンク先の勝手に採掘君を作って開いておくと、一々gethに戻らず済む

スクリーンショット 2018-03-27 19.34.48.png

採掘が終わるとコントラクトが表示される。
赤いのがガスが必要なメソッド、青いのがガス不要なメソッド

スクリーンショット 2018-03-27 19.39.32.png

購入してみる

1 画面右上Boxの下部にValueがあるので、ここで 「1」 を入力し、単位を「Ether」にする
2 右下のコントラクトからmintを押してみる(fallbackでもいい)
3 トランザクションが発生しているので掘る
リンク先の勝手に採掘君を作って開いておくと、一々gethに戻らず済む

確認

右タブを下にスクロールしていくと、確認用のメソッドがあるので適当に確認してみる。

スクリーンショット 2018-03-27 19.45.55.png

  • 青いメソッドはトランザクションが発生しないため、掘る必要はない。
  • 赤いメソッドはトランザクションが発生するため、掘る。
  • payableのついているメソッドは、valueを指定しないと駄目

おまけ

デプロイ済のコントラクトは他のユーザも使える。
browser-solidity上ならユーザを切り替えるだけでいい。

gethなら

 hoge_contra = eth.contract(abi).at(address);

みたいにやれば「hoge_contra.method名」みたいな感じで色々やれる。

#例:
hoge_contra.updatemaru(トークンID, "名称", token_type, {from:【msg.senderに設定したいユーザ】, gas:3000000});

abiは「compile」→「HogeToken選択」→「Detail」から、ABIのとこの書類アイコン押すとコピー
スクリーンショット 2018-03-27 19.59.09.png

そのままgeth行って
一回テキストエディタか何かに移して、改行取り除いてから

#[abi = ]まで入力して
abi = ←ここにカーソルある状態でペースト

addressは「Run」タブの方にある。
スクリーンショット 2018-03-27 20.02.15.png

fallbackの上にあるやつの書類アイコンでコピーできる。

geth上で新規にデプロイしたいなら、DetailのDepoloyをコピって貼り付けたら多分いける。
→ 一回テキストに貼り付けて、コンストラクタの引数設定しないとだめだった。

※ gethだと妙な動きする時がままあるので調査中。

marutake
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした