👓

[Bunzz Decipher] Nounsの『NounsAuctionHouse』コントラクトを理解しよう!

2023/09/01に公開

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

https://cryptospells.jp/

今回はBunzzの新機能『DeCipher』を使用して、Nounsの「NounsSeeder」コントラクトを見てみようと思います。

DeCipher』はAIを使用してコントラクトのドキュメントを自動生成してくれるサービスです。

https://www.bunzz.dev/decipher

詳しい使い方に関しては以下の記事を参考にしてください!

https://zenn.dev/heku/articles/33266f0c19d523

今回使用する『DeCipher』のリンクは以下になります。

https://app.bunzz.dev/decipher/chains/1/addresses/0xF15a943787014461d94da08aD4040f79Cd7c124e

Etherscanのリンクは以下になります。

https://etherscan.io/address/0xF15a943787014461d94da08aD4040f79Cd7c124

概要

NounsAuctionHouse(名詞オークションハウス) コントラクトは、Nounsエコシステムの中で大切な部分で、デジタルアセットの所有権を表す特別なERC721トークンである「Nouns」をオークション形式で取引するための仕組みを提供します。
このコントラクトは、買い手と売り手の両方にとって、公平で安全なオークションプロセスを実現するために設計されています。

目的

NounsAuctionHouseコントラクトのメインの目的は、ユーザーがNounsをオークションを通じて取引できる分散型のマーケットプレイスを提供することです。
これによって、Nounの所有者は自分のトークンを売りたい場合に、それを購入したい潜在的な買い手と繋げる役割を果たします。

責務

  • オークションの管理
    NounsAuctionHouseコントラクトは、新しいオークションを開始したり、入札を受け付けたり、最高額の入札を決定するなど、オークションの全体的なプロセスを担当し、オークションが公正で透明なものとなるようにします。

  • 入札の処理
    コントラクトは入札プロセスを管理し、ユーザーが興味を持っているNounsに対して入札を行う仕組みを提供します。
    最高入札額を記録し、新たな高額入札がある場合にはそれを更新します。

  • オークションの完了
    オークションが終了すると、コントラクトは最高入札者を決定し、その最高額でNounの所有権を移転させます。
    また、オークションの収益を売り手や関係者に適切に分配する役割も果たします。

  • セキュリティと保護
    コントラクトにはセキュリティのための対策が組み込まれており、PausableUpgradeableReentrancyGuardUpgradeableなどのライブラリを用いて、許可されていないアクセスや攻撃から守られます。

  • 所有権とアップグレード可能性
    NounsAuctionHouseコントラクトは、OwnableUpgradeableInitializableなどのライブラリを使って、機能の変更が認められた個人のみが行えるように設計されています。
    これにより、将来的なアップグレードや改善が可能となります。

  • 他のコントラクトとの統合
    NounsAuctionHouseコントラクトは、INounsTokenIERC20IERC721IWETHINounsDescriptorINounsSeederなどの他のコントラクトと連携しています。
    これにより、円滑な相互作用が実現され、Nounsエコシステム全体の運用がスムーズに行われます。

使い方

NounsAuctionHouseコントラクトは、特別なERC721トークンである「Nouns」のオークションを行うためのスマートコントラクトです。
このコントラクトは、ユーザーがNounsのオークションを自分で作成して参加できる仕組みを提供し、最高額の入札をしたユーザーがオークションに勝ち、そのNounトークンの所有権を得ることができます。

目標

NounsAuctionHouseコントラクトの目標は、Nounsをオークションを通じて取引するための分散型のマーケットプレイスを提供することです。
ユーザーは自分の持つNounsに対してオークションを作成し、開始価格や期間などの設定を行い、他のユーザーはそのオークションに参加してNounsに対して入札を行うことができます。
そしてオークションが終了すると、最高額の入札者がNounトークンの所有権を獲得します。

手順

Nounのオークションの分散型マーケットプレイスを実現するためには、次の手順が必要です。

  1. NounsAuctionHouseコントラクトをデプロイします。
  2. コントラクトを必要な設定で初期化します。
    NounsTokenコントラクトのアドレスやWETHコントラクトのアドレスなどを指定します。
  3. createAuction関数を呼び出して、Nounトークンのオークションを作成します。
    この際、NounのトークンIDや開始価格、オークションの期間などのパラメータを指定します。
  4. bid関数を呼び出して、オークションに参加してNounsに対して入札額とオークションIDを指定して入札を行います。
  5. オークションが終了するのを待ちます。
  6. もし最高額の入札者に対して、claim関数を呼び出してNounトークンを送ります。
  7. オークションの作成者がオークションをキャンセルする場合は、cancelAuction関数を呼び出してオークションを取り消し、Nounトークンを戻します。

関数

createAuction

Nounトークンの新しいオークションを作成します。

パラメータ

  • tokenId
    • オークションにかけるNounトークンのID。
  • duration
    • オークションの期間(秒単位)。
  • reservePrice
    • オークションの開始価格。
  • extensionDuration
    -オークション終了間際に入札がある場合に延長する期間。
  • auctionCurrency
    • 入札に使用されるERC20トークンのアドレス。

戻り値

  • auctionId
    • 作成されたオークションのID。

bid

既存のオークションに入札を行います。

パラメータ

  • auctionId
    • 入札するオークションのID。
  • amount
    • 入札額(オークション通貨で指定)。
      戻り値
      なし。

claim`

オークションに勝利した後、Nounトークンを落札者に送ります。

パラメータ

  • auctionId
    • 送るNounトークンのオークションID。

戻り値
なし。

cancelAuction

既存のオークションをキャンセルし、Nounトークンを取り戻します。

パラメータ

  • auctionId
    • キャンセルするオークションのID。

戻り値
なし。

イベント

AuctionCreated

新しいオークションが作成された時に発行されるイベントです。

パラメータ

  • auctionId
    • 作成されたオークションのID。
  • tokenId
    • オークション中のNounトークンのID。
  • seller
    • オークションを作成した売り手のアドレス。
  • reservePrice
    • オークションの開始価格。
  • duration
    • オークションの期間(秒単位)。
  • extensionDuration
    • オークション終了間際に入札がある場合に延長される期間。
  • auctionCurrency
    • 入札に使用されるERC20トークンのアドレス。

AuctionBid

オークションに入札が行われた時に発行されるイベントです。

パラメータ

  • auctionId
    • オークションのID。
  • bidder
    • 入札者のアドレス。
  • amount
    • 入札額(オークション通貨で指定)。

AuctionExtended

オークションが延長された時に発行されるイベントです。

パラメータ

  • auctionId
    • オークションのID。
  • extensionDuration
    • 延長された期間(秒単位)。

AuctionEnded

オークションが終了した時に発行されるイベントです。

パラメータ

  • auctionId
    • オークションのID。
  • tokenId
    • オークション中のNounトークンのID。
  • winner
    • 最高額の入札者のアドレス。
  • amount
    • 最高額の入札額(オークション通貨で指定)。

AuctionCanceled

オークションがキャンセルされた時に発行されるイベントです。

パラメータ

  • auctionId
    • キャンセルされたオークションのID。
  • tokenId
    • キャンセルされたオークション中のNounトークンのID。
  • canceller
    • オークションをキャンセルした作成者のアドレス。

関連EIP/ERC

以下は各情報を日本語に翻訳したリストです。

  • ERC721
    • オークション中のNounトークンはERC721トークンです。
  • ERC20
    • 入札に使用される通貨は任意のERC20トークンを使用できます。
  • EIP165
    • コントラクトはERC165インターフェースを実装してインターフェース検出をサポートしています。
  • EIP721
    • コントラクトはNounトークンとのやり取りにERC721インターフェースを実装しています。
  • EIP1155
    • コントラクトはNounトークンとのやり取りにERC1155インターフェースを実装しています。
  • EIP2981
    • コントラクトはNounトークンのロイヤルティ情報のためにERC2981インターフェースを実装しています。

パラメーター

なし。

コントラクト

NounsAuctionHouse

nouns

INounsToken public nouns;

概要
ERC721トークンコントラクトへのインターフェース。

詳細
ERC721トークンコントラクトへのインターフェースを保持します。
Nounsトークンは、オークションで取引されるERC721トークンであり、このインターフェースを通じてトークンの情報や操作にアクセスできます。


weth

address public weth;

概要
WETHコントラクトのアドレス。

詳細
WETH(Wrapped Ether) トークンのコントラクトアドレスを保持します。
WETHは、EtherERC20トークンとして扱うための標準的な方法です。
このアドレスを使用して、入札にWETHを使用することができます。


timeBuffer

uint256 public timeBuffer;

概要
新しい入札が作成された後、オークションの終了までに残る最小の時間。

詳細
オークションの最後の入札が作成された後、オークションの終了までに最低限残るべき時間を示します。
これにより、入札競争が終了する前に新しい入札が行われる可能性を確保します。


reservePrice

uint256 public reservePrice;

概要
オークションで受け入れられる最小価格。

詳細
オークションで受け入れられる最小価格を示します。
入札がこの価格より低い場合、オークションは成立しません。


minBidIncrementPercentage

uint8 public minBidIncrementPercentage;

概要
直前の入札額と現在の入札額の最小パーセンテージ差。

詳細
直前の入札額と現在の入札額との最小パーセンテージ差を示します。
入札額がこの差よりも少ない場合、新しい入札は受け入れられません。


duration

uint256 public duration;

概要
単一のオークションの期間。

詳細
単一のオークションの期間を示します。
オークションはこの期間内に終了する必要があります。


auction

INounsAuctionHouse.Auction public auction;

概要
アクティブなオークションの情報を保持する構造体。

詳細
アクティブなオークションの情報を保持するための構造体です。
アクティブなオークションとは、現在進行中のオークションを指します。
この構造体には、オークションの詳細な情報が含まれています。


initialize

initialize
function initialize(
        INounsToken _nouns,
        address _weth,
        uint256 _timeBuffer,
        uint256 _reservePrice,
        uint8 _minBidIncrementPercentage,
        uint256 _duration
    ) external initializer {
        __Pausable_init();
        __ReentrancyGuard_init();
        __Ownable_init();

        _pause();

        nouns = _nouns;
        weth = _weth;
        timeBuffer = _timeBuffer;
        reservePrice = _reservePrice;
        minBidIncrementPercentage = _minBidIncrementPercentage;
        duration = _duration;
}

概要
オークションハウスとベースコントラクトを初期化し、設定値を設定してコントラクトを一時停止する関数。
一度だけ呼び出すことができます。

詳細
この関数はコントラクトの初期化手続きを行います。
具体的には、PausableReentrancyGuardOwnableの初期化を行い、コントラクトを一時停止状態にします。

引数

  • _nouns
    • Nounsトークンを表すコントラクトアドレス。
  • _weth
    • Wrapped Ether(WETH) のコントラクトアドレス。
  • _timeBuffer
    • オークション終了前の時間バッファ。
  • _reservePrice
    • 最低落札価格。
  • _minBidIncrementPercentage
    • 最低入札増加率(%)。
  • _duration
    • オークションの期間。

settleCurrentAndCreateNewAuction

settleCurrentAndCreateNewAuction
function settleCurrentAndCreateNewAuction() external override nonReentrant whenNotPaused {
        _settleAuction();
        _createAuction();
}

概要
現在のオークションを決済し、新しいNounを作成してオークションに出品する関数。

詳細
この関数は以下の手順で実行されます。

  1. _settleAuction()関数を呼び出して現在のオークションを決済します。
  2. _createAuction()関数を呼び出して新しいNounを作成し、オークションに出品します。

settleAuction

settleAuction
function settleAuction() external override whenPaused nonReentrant {
        _settleAuction();
}

概要
現在のオークションを決済する関数。

詳細
この関数は一時停止状態のコントラクト内でのみ呼び出すことができます。
以下の手順で実行されます。

  1. _settleAuction()関数を呼び出して現在のオークションを決済します。

createBid

createBid
function createBid(uint256 nounId) external payable override nonReentrant {
        INounsAuctionHouse.Auction memory _auction = auction;

        require(_auction.nounId == nounId, 'Noun not up for auction');
        require(block.timestamp < _auction.endTime, 'Auction expired');
        require(msg.value >= reservePrice, 'Must send at least reservePrice');
        require(
            msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100),
            'Must send more than last bid by minBidIncrementPercentage amount'
        );

        address payable lastBidder = _auction.bidder;

        // Refund the last bidder, if applicable
        if (lastBidder != address(0)) {
            _safeTransferETHWithFallback(lastBidder, _auction.amount);
        }

        auction.amount = msg.value;
        auction.bidder = payable(msg.sender);

        // Extend the auction if the bid was received within `timeBuffer` of the auction end time
        bool extended = _auction.endTime - block.timestamp < timeBuffer;
        if (extended) {
            auction.endTime = _auction.endTime = block.timestamp + timeBuffer;
        }

        emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended);

        if (extended) {
            emit AuctionExtended(_auction.nounId, _auction.endTime);
        }
}

概要
指定されたNounに対して入札を行う関数。
指定された金額でのみETHによる支払いを受け付けます。

詳細
この関数は以下の手順で実行されます。

  1. 現在のオークション情報を_auction変数から取得します。
  2. 入札対象のNounが指定されたNounであることを確認します。
  3. オークションの終了時間が過ぎていないことを確認します。
  4. 送信されたETHの量が最低落札価格以上であることを確認します。
  5. 送信されたETHの量が前回の入札額に最低入札増加率を適用して計算された金額以上であることを確認します。
  6. 前回の入札者にETHを返金します(前回の入札者がいる場合)。
  7. _auctionの金額と入札者を更新します。
  8. 入札がオークション終了時間からtimeBuffer以内に行われた場合、オークションの終了時間を延長します。
  9. 適切なイベントを発行します。

引数

  • nounId
    • 入札対象のNounのID。

pause

pause
function pause() external override onlyOwner {
        _pause();
}

概要
この関数はNounsオークションハウスを一時停止する関数。

詳細
オーナーによって呼び出されることができます。
実行すると、コントラクトの状態を一時停止状態に変更します。
一時停止中でも、既存のオークションの決済は誰でも行えますが、新しいオークションの開始はできません。


unpause

unpause
function unpause() external override onlyOwner {
        _unpause();

        if (auction.startTime == 0 || auction.settled) {
            _createAuction();
        }
}

概要
Nounsオークションハウスの一時停止を解除し、必要に応じて新しいオークションを開始する関数。

詳細
オーナーによって呼び出されることができます。
実行すると、コントラクトの状態を一時停止解除状態に変更します。
また、現在のオークション情報を確認し、オークションが開始されていない場合や既に決済済みの場合は、新しいオークションを開始します。


setTimeBuffer

setTimeBuffer
function setTimeBuffer(uint256 _timeBuffer) external override onlyOwner {
        timeBuffer = _timeBuffer;

        emit AuctionTimeBufferUpdated(_timeBuffer);
}

概要
オークションの時間バッファを設定する関数。

詳細
オーナーによって呼び出されることができます。
指定された時間バッファを変数timeBufferに設定します。

引数

  • _timeBuffer
    • 設定するオークションの時間バッファ。

setReservePrice

setReservePrice
function setReservePrice(uint256 _reservePrice) external override onlyOwner {
        reservePrice = _reservePrice;

        emit AuctionReservePriceUpdated(_reservePrice);
}

概要
オークションの最低落札価格を設定する関数。

詳細
オーナーによって呼び出されることができます。
指定された最低落札価格を変数reservePriceに設定します。

引数

  • _reservePrice
    • 設定する最低落札価格。

setMinBidIncrementPercentage

setMinBidIncrementPercentage
function setMinBidIncrementPercentage(uint8 _minBidIncrementPercentage) external override onlyOwner {
        minBidIncrementPercentage = _minBidIncrementPercentage;

        emit AuctionMinBidIncrementPercentageUpdated(_minBidIncrementPercentage);
}

概要
オークションの最低入札増加率を設定する関数。

詳細
オーナーによって呼び出されることができます。
指定された最低入札増加率を変数minBidIncrementPercentageに設定します。

引数

  • _minBidIncrementPercentage
    • 設定する最低入札増加率(%)。

_createAuction

_createAuction
function _createAuction() internal {
        try nouns.mint() returns (uint256 nounId) {
            uint256 startTime = block.timestamp;
            uint256 endTime = startTime + duration;

            auction = Auction({
                nounId: nounId,
                amount: 0,
                startTime: startTime,
                endTime: endTime,
                bidder: payable(0),
                settled: false
            });

            emit AuctionCreated(nounId, startTime, endTime);
        } catch Error(string memory) {
            _pause();
        }
}

概要
オークションを作成する関数。

詳細
この関数は以下の手順で実行されます。

  1. nouns.mint()関数を呼び出してNounを新たに作成し、そのIDを取得します。
  2. オークションの開始時刻と終了時刻を計算します。
  3. オークションの詳細情報をauction変数に設定し、AuctionCreatedイベントを発行します。
  4. もしmint関数が失敗した場合(たとえば、ミンターがコントラクトの一時停止を行わずに更新された場合)、コントラクトを一時停止状態に変更します。

_settleAuction

_settleAuction
function _settleAuction() internal {
        INounsAuctionHouse.Auction memory _auction = auction;

        require(_auction.startTime != 0, "Auction hasn't begun");
        require(!_auction.settled, 'Auction has already been settled');
        require(block.timestamp >= _auction.endTime, "Auction hasn't completed");

        auction.settled = true;

        if (_auction.bidder == address(0)) {
            nouns.burn(_auction.nounId);
        } else {
            nouns.transferFrom(address(this), _auction.bidder, _auction.nounId);
        }

        if (_auction.amount > 0) {
            _safeTransferETHWithFallback(owner(), _auction.amount);
        }

        emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount);
}

概要
オークションを決済し、入札を確定させてオーナー送金する関数。

詳細
この関数は以下の手順で実行されます。

  1. 現在のオークション情報を_auction変数から取得します。
  2. オークションが開始されていることを確認します。
  3. オークションが既に決済済みでないことを確認します。
  4. オークションの終了時刻が過ぎていることを確認します。
  5. オークションの状態を決済済みに更新します。
  6. もし入札者がいない場合、Nounを焼却します。
  7. 入札者がいる場合、Nounを入札者に転送します。
  8. 入札者に支払われる金額が0より大きい場合、オーナーに支払います。
  9. 適切なイベントを発行します。

_safeTransferETHWithFallback

_safeTransferETHWithFallback
function _safeTransferETHWithFallback(address to, uint256 amount) internal {
        if (!_safeTransferETH(to, amount)) {
            IWETH(weth).deposit{ value: amount }();
            IERC20(weth).transfer(to, amount);
        }
}

概要
ETHを転送する関数。
ETHの転送に失敗した場合、ETHWETHに変換して再試行します。

詳細
この関数は以下の手順で実行されます。

  1. _safeTransferETH関数を使用してETHを指定されたアドレスに転送します。
  2. もしETHの転送が失敗した場合、ETHWETHに変換して再試行します。

引数

  • to
    • ETHを転送する対象のアドレス。
  • amount
    • 転送するETHの量。

_safeTransferETH

_safeTransferETH
function _safeTransferETH(address to, uint256 value) internal returns (bool) {
        (bool success, ) = to.call{ value: value, gas: 30_000 }(new bytes(0));
        return success;
}

概要
ETHを転送し、その成功ステータスを返す関数。

詳細
この関数は以下の手順で実行されます。

  1. 指定されたアドレスに対して指定された量のETHを転送します。
  2. 転送の成功ステータスを返します。

引数

  • to
    • ETHを転送する対象のアドレス。
  • value
    • 転送するETHの量。

戻り値
ETHの転送が成功した場合はtrue、失敗した場合はfalseを返します。


イベント

AuctionCreated

AuctionCreated
event AuctionCreated(uint256 indexed nounId, uint256 startTime, uint256 endTime);

概要
新しいオークションが作成されたときに発行されるイベント。

パラメータ

  • nounId
    • オークションに出品されたNounのID。
  • startTime
    • オークションの開始時刻。
  • endTime
    • オークションの終了時刻。

AuctionBid

AuctionBid
event AuctionBid(uint256 indexed nounId, address sender, uint256 value, bool extended);

概要
Nounに対して入札が行われたときに発行されるイベント。

パラメータ詳細

  • nounId
    • 入札されたNounのID。
  • sender
    • 入札を行ったアドレス。
  • value
    • 入札されたETHの金額。
  • extended
    • 入札が時間バッファ内で行われたかどうか。

AuctionExtended

AuctionExtended
event AuctionExtended(uint256 indexed nounId, uint256 endTime);

概要
オークションの終了時間が時間バッファ内で延長されたときに発行されるイベント。

パラメータ

  • nounId
    • 対象となるNounのID。
  • endTime
    • 延長された後のオークションの終了時刻。

AuctionSettled

AuctionSettled
event AuctionSettled(uint256 indexed nounId, address winner, uint256 amount);

概要
オークションが決済されたときに発行されるイベント。

パラメータ

  • nounId
    • 決済されたNounのID。
  • winner
    • オークションに勝利したアドレス。
  • amount
    • 支払われる金額。

AuctionTimeBufferUpdated

AuctionTimeBufferUpdated
event AuctionTimeBufferUpdated(uint256 timeBuffer);

概要
オークションの時間バッファが更新されたときに発行されるイベント。

詳細

  • timeBuffer
    • 更新された時間バッファ。

AuctionReservePriceUpdated

AuctionReservePriceUpdated
event AuctionReservePriceUpdated(uint256 reservePrice);

概要
オークションの最低落札価格が更新されたときに発行されるイベント。

パラメータ

  • reservePrice
    • 更新された最低落札価格。

AuctionMinBidIncrementPercentageUpdated

AuctionMinBidIncrementPercentageUpdated
event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage);

概要
オークションの最低入札増加率が更新されたときに発行されるイベント。

詳細

  • minBidIncrementPercentage
    • 更新された最低入札増加率。

コード

インターフェース

  • /contracts/interfaces

INounsAuctionHouse.sol

INounsAuctionHouse.sol
// SPDX-License-Identifier: GPL-3.0

/// @title Interface for Noun Auction Houses

/*********************************
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░██░░░████░░██░░░████░░░ *
 * ░░██████░░░████████░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 *********************************/

pragma solidity ^0.8.6;

interface INounsAuctionHouse {
    struct Auction {
        // ID for the Noun (ERC721 token ID)
        uint256 nounId;
        // The current highest bid amount
        uint256 amount;
        // The time that the auction started
        uint256 startTime;
        // The time that the auction is scheduled to end
        uint256 endTime;
        // The address of the current highest bid
        address payable bidder;
        // Whether or not the auction has been settled
        bool settled;
    }

    event AuctionCreated(uint256 indexed nounId, uint256 startTime, uint256 endTime);

    event AuctionBid(uint256 indexed nounId, address sender, uint256 value, bool extended);

    event AuctionExtended(uint256 indexed nounId, uint256 endTime);

    event AuctionSettled(uint256 indexed nounId, address winner, uint256 amount);

    event AuctionTimeBufferUpdated(uint256 timeBuffer);

    event AuctionReservePriceUpdated(uint256 reservePrice);

    event AuctionMinBidIncrementPercentageUpdated(uint256 minBidIncrementPercentage);

    function settleAuction() external;

    function settleCurrentAndCreateNewAuction() external;

    function createBid(uint256 nounId) external payable;

    function pause() external;

    function unpause() external;

    function setTimeBuffer(uint256 timeBuffer) external;

    function setReservePrice(uint256 reservePrice) external;

    function setMinBidIncrementPercentage(uint8 minBidIncrementPercentage) external;
}

INounsToken.sol

INounsToken.sol
// SPDX-License-Identifier: GPL-3.0

/// @title Interface for NounsToken

/*********************************
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░██░░░████░░██░░░████░░░ *
 * ░░██████░░░████████░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 *********************************/

pragma solidity ^0.8.6;

import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import { INounsDescriptor } from './INounsDescriptor.sol';
import { INounsSeeder } from './INounsSeeder.sol';

interface INounsToken is IERC721 {
    event NounCreated(uint256 indexed tokenId, INounsSeeder.Seed seed);

    event NounBurned(uint256 indexed tokenId);

    event NoundersDAOUpdated(address noundersDAO);

    event MinterUpdated(address minter);

    event MinterLocked();

    event DescriptorUpdated(INounsDescriptor descriptor);

    event DescriptorLocked();

    event SeederUpdated(INounsSeeder seeder);

    event SeederLocked();

    function mint() external returns (uint256);

    function burn(uint256 tokenId) external;

    function dataURI(uint256 tokenId) external returns (string memory);

    function setNoundersDAO(address noundersDAO) external;

    function setMinter(address minter) external;

    function lockMinter() external;

    function setDescriptor(INounsDescriptor descriptor) external;

    function lockDescriptor() external;

    function setSeeder(INounsSeeder seeder) external;

    function lockSeeder() external;
}

IWETH.sol

IWETH.sol
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.6;

interface IWETH {
    function deposit() external payable;

    function withdraw(uint256 wad) external;

    function transfer(address to, uint256 value) external returns (bool);
}

INounsDescriptor.sol

INounsDescriptor.sol
// SPDX-License-Identifier: GPL-3.0

/// @title Interface for NounsDescriptor

/*********************************
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░██░░░████░░██░░░████░░░ *
 * ░░██████░░░████████░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 *********************************/

pragma solidity ^0.8.6;

import { INounsSeeder } from './INounsSeeder.sol';

interface INounsDescriptor {
    event PartsLocked();

    event DataURIToggled(bool enabled);

    event BaseURIUpdated(string baseURI);

    function arePartsLocked() external returns (bool);

    function isDataURIEnabled() external returns (bool);

    function baseURI() external returns (string memory);

    function palettes(uint8 paletteIndex, uint256 colorIndex) external view returns (string memory);

    function backgrounds(uint256 index) external view returns (string memory);

    function bodies(uint256 index) external view returns (bytes memory);

    function accessories(uint256 index) external view returns (bytes memory);

    function heads(uint256 index) external view returns (bytes memory);

    function glasses(uint256 index) external view returns (bytes memory);

    function backgroundCount() external view returns (uint256);

    function bodyCount() external view returns (uint256);

    function accessoryCount() external view returns (uint256);

    function headCount() external view returns (uint256);

    function glassesCount() external view returns (uint256);

    function addManyColorsToPalette(uint8 paletteIndex, string[] calldata newColors) external;

    function addManyBackgrounds(string[] calldata backgrounds) external;

    function addManyBodies(bytes[] calldata bodies) external;

    function addManyAccessories(bytes[] calldata accessories) external;

    function addManyHeads(bytes[] calldata heads) external;

    function addManyGlasses(bytes[] calldata glasses) external;

    function addColorToPalette(uint8 paletteIndex, string calldata color) external;

    function addBackground(string calldata background) external;

    function addBody(bytes calldata body) external;

    function addAccessory(bytes calldata accessory) external;

    function addHead(bytes calldata head) external;

    function addGlasses(bytes calldata glasses) external;

    function lockParts() external;

    function toggleDataURIEnabled() external;

    function setBaseURI(string calldata baseURI) external;

    function tokenURI(uint256 tokenId, INounsSeeder.Seed memory seed) external view returns (string memory);

    function dataURI(uint256 tokenId, INounsSeeder.Seed memory seed) external view returns (string memory);

    function genericDataURI(
        string calldata name,
        string calldata description,
        INounsSeeder.Seed memory seed
    ) external view returns (string memory);

    function generateSVGImage(INounsSeeder.Seed memory seed) external view returns (string memory);
}

INounsSeeder.sol

INounsSeeder.sol
// SPDX-License-Identifier: GPL-3.0

/// @title Interface for NounsSeeder

/*********************************
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░██░░░████░░██░░░████░░░ *
 * ░░██████░░░████████░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 *********************************/

pragma solidity ^0.8.6;

import { INounsDescriptor } from './INounsDescriptor.sol';

interface INounsSeeder {
    struct Seed {
        uint48 background;
        uint48 body;
        uint48 accessory;
        uint48 head;
        uint48 glasses;
    }

    function generateSeed(uint256 nounId, INounsDescriptor descriptor) external view returns (Seed memory);
}

NounsAuctionHouse.sol

  • /contracts/NounsAuctionHouse.sol

NounsAuctionHouse.sol

NounsAuctionHouse.sol
// SPDX-License-Identifier: GPL-3.0

/// @title The Nouns DAO auction house

/*********************************
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░██░░░████░░██░░░████░░░ *
 * ░░██████░░░████████░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░██░░██░░░████░░██░░░████░░░ *
 * ░░░░░░█████████░░█████████░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
 *********************************/

// LICENSE
// NounsAuctionHouse.sol is a modified version of Zora's AuctionHouse.sol:
// https://github.com/ourzora/auction-house/blob/54a12ec1a6cf562e49f0a4917990474b11350a2d/contracts/AuctionHouse.sol
//
// AuctionHouse.sol source code Copyright Zora licensed under the GPL-3.0 license.
// With modifications by Nounders DAO.

pragma solidity ^0.8.6;

import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol';
import { ReentrancyGuardUpgradeable } from '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol';
import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import { INounsAuctionHouse } from './interfaces/INounsAuctionHouse.sol';
import { INounsToken } from './interfaces/INounsToken.sol';
import { IWETH } from './interfaces/IWETH.sol';

contract NounsAuctionHouse is INounsAuctionHouse, PausableUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable {
    // The Nouns ERC721 token contract
    INounsToken public nouns;

    // The address of the WETH contract
    address public weth;

    // The minimum amount of time left in an auction after a new bid is created
    uint256 public timeBuffer;

    // The minimum price accepted in an auction
    uint256 public reservePrice;

    // The minimum percentage difference between the last bid amount and the current bid
    uint8 public minBidIncrementPercentage;

    // The duration of a single auction
    uint256 public duration;

    // The active auction
    INounsAuctionHouse.Auction public auction;

    /**
     * @notice Initialize the auction house and base contracts,
     * populate configuration values, and pause the contract.
     * @dev This function can only be called once.
     */
    function initialize(
        INounsToken _nouns,
        address _weth,
        uint256 _timeBuffer,
        uint256 _reservePrice,
        uint8 _minBidIncrementPercentage,
        uint256 _duration
    ) external initializer {
        __Pausable_init();
        __ReentrancyGuard_init();
        __Ownable_init();

        _pause();

        nouns = _nouns;
        weth = _weth;
        timeBuffer = _timeBuffer;
        reservePrice = _reservePrice;
        minBidIncrementPercentage = _minBidIncrementPercentage;
        duration = _duration;
    }

    /**
     * @notice Settle the current auction, mint a new Noun, and put it up for auction.
     */
    function settleCurrentAndCreateNewAuction() external override nonReentrant whenNotPaused {
        _settleAuction();
        _createAuction();
    }

    /**
     * @notice Settle the current auction.
     * @dev This function can only be called when the contract is paused.
     */
    function settleAuction() external override whenPaused nonReentrant {
        _settleAuction();
    }

    /**
     * @notice Create a bid for a Noun, with a given amount.
     * @dev This contract only accepts payment in ETH.
     */
    function createBid(uint256 nounId) external payable override nonReentrant {
        INounsAuctionHouse.Auction memory _auction = auction;

        require(_auction.nounId == nounId, 'Noun not up for auction');
        require(block.timestamp < _auction.endTime, 'Auction expired');
        require(msg.value >= reservePrice, 'Must send at least reservePrice');
        require(
            msg.value >= _auction.amount + ((_auction.amount * minBidIncrementPercentage) / 100),
            'Must send more than last bid by minBidIncrementPercentage amount'
        );

        address payable lastBidder = _auction.bidder;

        // Refund the last bidder, if applicable
        if (lastBidder != address(0)) {
            _safeTransferETHWithFallback(lastBidder, _auction.amount);
        }

        auction.amount = msg.value;
        auction.bidder = payable(msg.sender);

        // Extend the auction if the bid was received within `timeBuffer` of the auction end time
        bool extended = _auction.endTime - block.timestamp < timeBuffer;
        if (extended) {
            auction.endTime = _auction.endTime = block.timestamp + timeBuffer;
        }

        emit AuctionBid(_auction.nounId, msg.sender, msg.value, extended);

        if (extended) {
            emit AuctionExtended(_auction.nounId, _auction.endTime);
        }
    }

    /**
     * @notice Pause the Nouns auction house.
     * @dev This function can only be called by the owner when the
     * contract is unpaused. While no new auctions can be started when paused,
     * anyone can settle an ongoing auction.
     */
    function pause() external override onlyOwner {
        _pause();
    }

    /**
     * @notice Unpause the Nouns auction house.
     * @dev This function can only be called by the owner when the
     * contract is paused. If required, this function will start a new auction.
     */
    function unpause() external override onlyOwner {
        _unpause();

        if (auction.startTime == 0 || auction.settled) {
            _createAuction();
        }
    }

    /**
     * @notice Set the auction time buffer.
     * @dev Only callable by the owner.
     */
    function setTimeBuffer(uint256 _timeBuffer) external override onlyOwner {
        timeBuffer = _timeBuffer;

        emit AuctionTimeBufferUpdated(_timeBuffer);
    }

    /**
     * @notice Set the auction reserve price.
     * @dev Only callable by the owner.
     */
    function setReservePrice(uint256 _reservePrice) external override onlyOwner {
        reservePrice = _reservePrice;

        emit AuctionReservePriceUpdated(_reservePrice);
    }

    /**
     * @notice Set the auction minimum bid increment percentage.
     * @dev Only callable by the owner.
     */
    function setMinBidIncrementPercentage(uint8 _minBidIncrementPercentage) external override onlyOwner {
        minBidIncrementPercentage = _minBidIncrementPercentage;

        emit AuctionMinBidIncrementPercentageUpdated(_minBidIncrementPercentage);
    }

    /**
     * @notice Create an auction.
     * @dev Store the auction details in the `auction` state variable and emit an AuctionCreated event.
     * If the mint reverts, the minter was updated without pausing this contract first. To remedy this,
     * catch the revert and pause this contract.
     */
    function _createAuction() internal {
        try nouns.mint() returns (uint256 nounId) {
            uint256 startTime = block.timestamp;
            uint256 endTime = startTime + duration;

            auction = Auction({
                nounId: nounId,
                amount: 0,
                startTime: startTime,
                endTime: endTime,
                bidder: payable(0),
                settled: false
            });

            emit AuctionCreated(nounId, startTime, endTime);
        } catch Error(string memory) {
            _pause();
        }
    }

    /**
     * @notice Settle an auction, finalizing the bid and paying out to the owner.
     * @dev If there are no bids, the Noun is burned.
     */
    function _settleAuction() internal {
        INounsAuctionHouse.Auction memory _auction = auction;

        require(_auction.startTime != 0, "Auction hasn't begun");
        require(!_auction.settled, 'Auction has already been settled');
        require(block.timestamp >= _auction.endTime, "Auction hasn't completed");

        auction.settled = true;

        if (_auction.bidder == address(0)) {
            nouns.burn(_auction.nounId);
        } else {
            nouns.transferFrom(address(this), _auction.bidder, _auction.nounId);
        }

        if (_auction.amount > 0) {
            _safeTransferETHWithFallback(owner(), _auction.amount);
        }

        emit AuctionSettled(_auction.nounId, _auction.bidder, _auction.amount);
    }

    /**
     * @notice Transfer ETH. If the ETH transfer fails, wrap the ETH and try send it as WETH.
     */
    function _safeTransferETHWithFallback(address to, uint256 amount) internal {
        if (!_safeTransferETH(to, amount)) {
            IWETH(weth).deposit{ value: amount }();
            IERC20(weth).transfer(to, amount);
        }
    }

    /**
     * @notice Transfer ETH and return the success status.
     * @dev This function only forwards 30,000 gas to the callee.
     */
    function _safeTransferETH(address to, uint256 value) internal returns (bool) {
        (bool success, ) = to.call{ value: value, gas: 30_000 }(new bytes(0));
        return success;
    }
}

最後に

今回の記事では、Bunzzの新機能『DeCipher』を使用して、Nounsの「NounsAuctionHouse」のコントラクトを見てきました。
いかがだったでしょうか?
今後も特定のNFTやコントラクトをピックアップしてまとめて行きたいと思います。

普段はブログやQiitaでブロックチェーンやAIに関する記事を挙げているので、よければ見ていってください!

https://chaldene.net/

https://qiita.com/cardene

CryptoGames

Discussion