Solidityでオークションを実装しました

Ethereumネットワークでのコントラクト開発に使われるSolidityでオークションを実装してみました。まあSolidityのページにあるexampleをほぼそのまま写経してコンパイルして確認してみただけなのですが、実際に動かすことで理解が深まるかと思います。 solidity-jp.readthedocs.io

実装したものは以下になります。 github.com

準備

コンパイラの準備

Solidityのページでは以下のページでコンパイラーについて説明があるのですが、自分はtruffleのコンパイラを使っています。

Installing the Solidity Compiler — Solidity 0.5.4 ドキュメント

事前にnpmがインストールされているのであればnpm install -g truffleでtruffleをグローバルインストールするだけで使えます。 Truffle | Truffle Suite

ネットワーク

開発したコントラクトのデプロイ先として本番のネットワークおよびテストネットワーク、Mockネットワークがあり開発時はテストネットワーク、Mockネットワークを使います。テストネットワークについてはKovanやRinkebyなどがあり、MockネットワークとしてはGanacheがあります。

www.trufflesuite.com

テストネットワークはアカウント作成し開発に使うETHを発行してもらう必要があるのですがMockネットワークのGanacheは起動するだけで自動で複数の開発アカウントが準備されて手軽のため、今回はGanacheを使います。ただMockネットワークはスマートコントラクトが外部データを取得するためのオラクルが利用できないなどの制限があるようです。

以下のページにて代表的なオラクルであるchainlinkではMockネットワーク上で動かないとあります。 Running a Chainlink Node | Chainlink Documentation

Running Chainlink Node on Ganache

Ganache is a mock testnet and it doesn't work with Chainlink because of that. To use the features of the network, you need to deploy your contract on a real environment: one of the testnets or mainnets. The full list of supported environments can be found here.

ウォレット

アカウント管理用のウォレットとしてブラウザ拡張機能のMetamaskを使いました。

chrome.google.com

実装

オークションの実装は以下になります。

pragma solidity ^0.5.4;

contract Auction {
    string public title;
    string public text;
    uint256 public auctionEndTime;
    address payable public beneficiary;

    address payable public highestBidder;
    uint256 public highestBid;

    mapping(address => uint256) pendingReturns;

    bool end = false;

    event HighestBidIncreased(address bidder, uint256 amount);
    event AuctionEnded(address winner, uint256 amount);

    constructor(
        string memory _title,
        string memory _text,
        uint256 _biddingTime,
        uint256 _startPrice,
        address payable _beneficiary
    ) public {
        title = _title;
        text = _text;
        auctionEndTime = now + _biddingTime * 60;
        beneficiary = _beneficiary;
        highestBid = _startPrice;
    }

    function bid() public payable {
        require(now <= auctionEndTime, "Auction already ended.");
        require(msg.value > highestBid, "There already is a higher bid.");

        if (highestBidder != address(0)) {
            highestBidder.transfer(highestBid);
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    function auctionEnd() public {
        require(now >= auctionEndTime, "Auction not yet ended.");
        require(!end, "auctionEnd has already been called.");
        require(
            beneficiary == msg.sender,
            "Only beneficiaries can end the auction."
        );

        end = true;
        emit AuctionEnded(highestBidder, highestBid);

        if (highestBidder != address(0)) {
            beneficiary.transfer(highestBid);
        }
    }

    function getInfo()
        public
        view
        returns (
            string memory,
            string memory,
            uint256,
            address,
            address,
            uint256,
            bool
        )
    {
        return (
            title,
            text,
            auctionEndTime,
            beneficiary,
            highestBidder,
            highestBid,
            end
        );
    }
}

constructorはデプロイ時に呼ばれ名前と説明用テキスト、出品時間、出品者、始値を指定しています。

bidは入札時に呼ばれrequireで出品時間内および現在の値段より上かをチェックしまして、有効であればその時点の最高額入札者に返金し、現在の値段と最高額入札者を更新してクライアント側に通知を行うためemit HighestBidIncreasedを呼び出します。

auctionEndは出品時間が過ぎた後に出品者が呼び出すことで出品者に入札金額が送金されオークションが終了となります。

ビルド

スマートコントラクトの開発ではコントラクトのビルドしABI定義をネットワークにデプロイします。スマートコントラクトのビルドはtruffleを使うのですが以下のコマンドで実行します。

$ truffle compile

truffleでコンパイルするとbuild/contracts/Auction.jsonが生成されます。中身を見るとcontractName,abi,compiler等が含まれているのが確認できます。

クライアント

コントラクトのビルドが終わったらあとはクライアント側の実装になります。今回はweb3.jsを使ってブラウザ上で動かします。クライアントの実装は以下になります。

github.com

Metamaskのアカウントと同期

Metamaskのアカウントを取得するために以下のようにethereum.request({ method: 'eth_requestAccounts' })を実行します。ethereumはMetamaskがインストールされていたら参照できるようになります。

const syncAccount = () => {
    if (syncAccountTimer) {
        clearInterval(syncAccountTimer);
    }
    syncAccountTimer = setInterval(() => {
        ethereum.request({ method: 'eth_requestAccounts' }).then(accounts => {
            const { account } = this.state;
            if (account != accounts[0]) {
                this.set({ account: accounts[0] });
            }

        });
    }, 1000);
}

これでブラウザ上でMetamaskのアカウントを変更すると画面上に表示されるアカウントが名画変更されるようになります。

コントラクトのデプロイ(出品)

ビルド結果のjsonファイルを読み込んでコントラクトを初期化してdeployメソッドを実行します。まずコントラクトを以下のように初期化し

const contract = new web3js.eth.Contract(cryptoAuctionABI.abi);

それからデプロイします。デプロイした後にコントラクトのアドレスが返ってくるのですが、これがないと対象のコントラクトにアクセスできないので保持しておきます。

contract.deploy({ data: cryptoAuctionABI.bytecode, arguments: [title, text, time, web3js.utils.toWei(price.toString(), "ether"), account] })
    .send({
        from: this.state.account,
        gasPrice: 20000000000
    }, (error, transactionHash) => { })
    .on('error', (error) => { })
    .on('transactionHash', (transactionHash) => { })
    .on('receipt', (receipt) => {
        // logBox.push("receipt: " + receipt.contractAddress);
        // console.log(receipt.contractAddress) // contains the new contract address
    })
    .on('confirmation', (confirmationNumber, receipt) => { })
    .then((newContractInstance) => {
        console.log(newContractInstance.options.address) // instance with the new contract address
        this.set({ contractAddress: this.state.contractAddress.concat(newContractInstance.options.address) });
    });

入札

次に入札ですが、入札するにはデプロイしたコントラクトのアドレスが必要で以下のようにアドレスを指定してコントラクトを初期化します。

new web3js.eth.Contract(コントラクトのABI定義, コントラクトアドレス)

それから以下のようにコントラクトの入札関数を呼び出します。コントラクト側の関数呼び出しとしてcontract.methods.メソッド名.sendcontract.methods.メソッド名.callの呼び出しがあるのですがsendとcallの違いとしてコントラクトの書き込みがあるか、書き込みがないかによって異なり、入札はコントラクトの書き込みがあるので今回はsendで呼び出しています。

contract.methods.bid().send({ from: account, value: web3js.utils.toWei(bidPrice, "ether") })
    .on("receipt", (result) => {
        console.info(result);
    })
    .on("error", (error) => {
        console.error(error);
    });

イベントハンドル

コントラクト側でemitしたイベントは以下のようにweb3.jsのクライアント側でハンドルすることが出来ます。

contract.events.allEvents({ filter: {} })
    .on("data", (event) => {
        if (event.event == "HighestBidIncreased") {
            logBox.push(`入札: アカウント[${event.returnValues.bidder}] bid[${Web3.utils.fromWei(event.returnValues.amount, 'ether')}]`)
        } else {
            logBox.push(`終了: アカウント[${event.returnValues.winner}] bid[${Web3.utils.fromWei(event.returnValues.amount, 'ether')}]`)
        }

        console.info(event.returnValues);
        this.getInfo(contract)
    }).on("error", console.error);

オークション情報取得

オークションの情報取得としてコントラクトで定義したgetInfo関数を呼び出してまして、getInfo関数はviewでコントラクトの書き込みがないのでcallで呼び出しています。

contract.methods.getInfo().call().then(info => {
    console.info(info);
    logBox.push(`get auction info: title[${info[0]}] text[${info[1]}] auctionEndTime[${new Date(1000 * info[2])}] beneficiary[${info[3]}] highestBidder[${info[4]}] highestBid[${info[5]}] end[${info[6]}]`)

    const title = info[0];
    const text = info[1];
    const time = (new Date(1000 * info[2]) - new Date()) / (1000 * 60);
    const price = Web3.utils.fromWei(info[5], 'ether');
    const beneficiary = info[3]
    const highestBidder = info[4]
    const end = info[6]
    this.set({ title: title, text: text, time: time, price: price, beneficiary: beneficiary, highestBidder: highestBidder, end: end });
});

オークション終了

最後にオークションの終了としてauctionEnd関数を呼び出しています、コントラクトへの書き込みがあるのでsendで呼び出しています。

contract.methods.auctionEnd().send({ from: account, value: 0 })
    .on("receipt", (result) => {
        console.info(result);
    })
    .on("error", (error) => {
        console.error(error);
    });