第18話:【実践編③】簡易オークションコントラクト開発に挑戦!状態管理の集大成

2025年7月31日

第18話:【実践編③】簡易オークションコントラクト開発に挑戦!状態管理の集大成

概要

【実践編③】これまでの知識を結集し、簡易オークションコントラクトを構築します。enumを用いた「準備中/開催中/終了」といった厳密な状態管理、block.timestampによる時間制御を実装。

さらに、payableな入札機能と、安全な「Pull-over-Push」パターンによる返金ロジックなど、動的で価値の移動を伴うDApp開発の集大成となる、総合的な実践レポートです。

目次
Solidity学習講座:最終版 目次(全20話)
Solidity学習講座:最終版 目次(全20話)

基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...

続きを見る

はじめに

実践編の第一弾「投票システム」、第二弾「独自トークン」と、あなたは着実にDApp開発の経験を積んできました。structmappingでデータを整理し、modifierrequireで安全性を確保し、そしてeventで外部に情報を伝える。これらの基本スキルは、あなたの強力な武器となっているはずです。

今回のプロジェクトは、これまでの集大成です。挑戦するのは、**「簡易イングリッシュ・オークション」**のスマートコントラクト。イングリッシュ・オークションとは、最も一般的な、価格を競り上げていき最高額の入札者が落札する形式のオークションです。

なぜオークションが「集大成」なのでしょうか? それは、これまでのDAppにはなかった、以下の重要な要素が含まれるからです。

  • 時間の概念: オークションには「開催期間」があります。block.timestampを使い、コントラクトが時間に応じて振る舞いを変えるロジックを実装します。
  • より複雑な状態管理: オークションは「準備中」「開催中」「終了」といった、明確な状態(ステート)を持ち、それらの間を一方通行で行き来します。この状態遷移を厳密に管理する必要があります。
  • 競合的な資金のやり取り: 複数の参加者がETHを入札し、最高入札者が常に更新されます。敗れた入札者への返金処理など、より高度で安全な資金管理が求められます。

このオークションコントラクトを完成させたとき、あなたはもはやSolidityの入門者ではありません。動的で、インタラクティブで、そして価値の移動を伴う、本格的なDAppをゼロから設計・実装できる、自信に満ちた開発者となっていることでしょう。

ステップ1:オークションの設計と状態の定義 (enum)

まず、オークションのルールと流れを決めます。

  1. 出品者(セラー): コントラクトをデプロイした人がセラー(兼管理者)です。
  2. オークション開始: セラーだけがオークションを開始できます。
  3. 入札: オークション開催期間中、誰でも入札できます。ただし、入札額は現在の最高額を上回らなければなりません。
  4. 返金: 他の誰かがより高い額で入札した場合、それまでの最高入札者はお金を取り戻せます(引き出し式)。
  5. オークション終了: 開催期間が終了したら、誰でもオークションを終了させることができます。最高入札額はセラーに送金され、その入札者が落札者となります。

この流れを管理するために、オークションの「状態」を明確に定義しましょう。こういう時に便利なのが、第8話の補足で触れた**enum(列挙型)**です。enumを使うと、数値(0, 1, 2...)の代わりに、意味のある名前で状態を表現でき、コードが格段に読みやすくなります。

Solidity
// オークションの状態を定義
enum State {
    Created, // 準備中
    Running, // 開催中
    Ended,   // 終了
    Canceled // キャンセル
}

これらの設計に基づき、必要な状態変数を定義します。

Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleAuction {
    // === データ構造と状態変数 ===
    enum State { Created, Running, Ended, Canceled }

    State public auctionState; // オークションの現在の状態

    address public immutable seller; // 出品者(変更不可)
    uint public auctionEndTime;     // オークション終了時刻

    address public highestBidder;    // 現在の最高入札者
    uint public highestBid;         // 現在の最高入札額

    // 入札額返金用の台帳
    mapping(address => uint) public pendingReturns;

    // === イベント ===
    event AuctionStarted(uint endTime);
    event NewBid(address indexed bidder, uint amount);
    event AuctionEnded(address winner, uint amount);
}

ここでimmutableという新しいキーワードが出てきました。address public immutable seller;のように宣言された変数は、constructor内で一度だけ値を設定でき、その後は二度と変更できなくなります。ガス効率が良く、不変であることを保証したい値に最適です。

ステップ2:constructorとオークション開始機能

constructorで、出品者(seller)を設定します。オークションの期間は、startAuction関数で設定することにしましょう。

Solidity
constructor() {
    seller = msg.sender;
    auctionState = State.Created;
}

function startAuction(uint _durationInMinutes) public {
    require(msg.sender == seller, "Only seller can start the auction.");
    require(auctionState == State.Created, "Auction has already been started or ended.");

    auctionState = State.Running;
    auctionEndTime = block.timestamp + (_durationInMinutes * 1 minutes);

    emit AuctionStarted(auctionEndTime);
}

_durationInMinutes * 1 minutesという書き方に注目してください。Solidityではseconds, minutes, hours, daysといった時間単位が使え、コードの可読性を高めてくれます。

ステップ3:入札機能 bid() の実装

ここが最も複雑な部分です。payable関数として、ETHの入札を受け付けます。

Solidity
function bid() public payable {
    // === Checks ===
    require(auctionState == State.Running, "Auction is not running.");
    require(block.timestamp < auctionEndTime, "Auction has already ended.");
    require(msg.value > highestBid, "Your bid must be higher than the current highest bid.");

    // === Effects ===
    // 前の最高入札者がいれば、その人の入札額を返金対象として記録
    if (highestBidder != address(0)) {
        pendingReturns[highestBidder] += highestBid;
    }

    // 新しい最高入札者と入札額を更新
    highestBidder = msg.sender;
    highestBid = msg.value;

    // === Interactions ===
    emit NewBid(msg.sender, msg.value);
}

新しい入札があると、それまでの最高入札者のhighestBidは、pendingReturnsマッピングに記録されます。これにより、「誰にいくら返金すべきか」をコントラクトが覚えておけるのです。

ステップ4:出金とオークション終了機能

前のステップでpendingReturnsに記録された資金は、入札者が自ら引き出す(withdraw)設計にします。コントラクト側から勝手に送金する(Push)のではなく、ユーザー側から取りに来てもらう(Pull)この**「Pull-over-Push」パターン**は、予期せぬ外部コントラクトの動作から自身を守るための、非常に重要なセキュリティパターンです。

Solidity
// 負けた入札者が、自分の資金を引き出すための関数
function withdraw() public {
    uint amount = pendingReturns[msg.sender];
    if (amount > 0) {
        // Effects: 先に残高を0にして、リエントランシー攻撃を防ぐ
        pendingReturns[msg.sender] = 0;

        // Interactions: 送金処理
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Withdrawal failed.");
    }
}

// オークションを終了させる関数
function endAuction() public {
    require(auctionState == State.Running, "Auction is not running.");
    require(block.timestamp >= auctionEndTime, "Auction has not ended yet.");

    auctionState = State.Ended;
    emit AuctionEnded(highestBidder, highestBid);

    // 落札者がいる場合のみ、最高入札額をセラーに送金
    if (highestBid > 0) {
        (bool success, ) = seller.call{value: highestBid}("");
        require(success, "Failed to send bid to seller.");
    }
}

withdraw関数では、送金前にpendingReturnsを0にすることで、「Checks-Effects-Interactions」パターンを遵守し、安全性を高めています。 endAuction関数は、誰でも呼び出せますが、時間が来ていないと実行できません。オークションが終了すると、保管されていたhighestBidが、晴れてセラーの元へと送金されます。

まとめ:状態を制する者は、DAppを制する

お疲れ様でした!あなたは今、時間、複数の状態、そして競合的な資金移動という、これまでのプロジェクトにはなかった複雑な要素を見事に操り、一つのオークションDAppを完成させました。

  • enum を使って、可読性の高い状態管理を行った。
  • block.timestamp と時間単位を使い、時間ベースのロジックを実装した。
  • payablebid関数で、競争的な資金の受け入れを実現した。
  • 「Pull-over-Push」パターンに基づいたwithdraw関数で、安全な返金処理を実装した。

この経験は、あなたのSolidityスキルが、静的な情報管理から、動的なアプリケーションの構築へと、大きくステップアップした証です。

さて、これまでの実践編で、あなたは投票、トークン、オークションといった、様々なDAppの心臓部を作ってきました。しかし、それらはすべて、私たちが独自に設計したものです。世界中のウォレットや取引所、他のDAppと互換性を持つためには、業界の**「標準規格」**に従う必要があります。

次回、**第19話「ERC20の世界へ!標準トークンのインターフェースを理解する」**では、ついにイーサリアムで最も有名で、最も重要なトークン規格である「ERC20」の世界に足を踏み入れます。あなたの作った独自トークンを、世界標準のトークンへと進化させる旅が始まります。

目次
Solidity学習講座:最終版 目次(全20話)
Solidity学習講座:最終版 目次(全20話)

基礎編 第1話:未来のインターネットへようこそ!Solidityとスマートコントラクトの全体像 第2話:準備は1分!ブラウザだけで開発できる「Remix IDE」の基本操作 第3話:記念すべき初コント ...

続きを見る

-Solidity