Smart Contractでランダムな値を取得する(Chainlink VRF)

PS/SLの佐々木です。

今回はChainlinkでランダムな値を生成するVRFを開発で使用する方法について解説しようと思います。

Chainlink VRFとは

ChainlinkVRFとはランダムな値を生成するための公正かつ検証可能な乱数生成をすることができます。

乱数生成と聞くと一般的なプログラム言語のライブラリとして組み込まれているためSolidityにも存在するように思えます。 しかしスマートコントラクトの理念として入力に対して常に同じ値を返すという考え方があり、ランダムな値というのはこの考えに反するためChainlinkのVRFを使用する必要があります。

VRFのUseCase

ChainlinkVRFのUseCaseとして、アプリケーションでルーレットのような機能を作成したり、サイコロのようなものを作成しようとすると乱数生成という機能が欲しくなるかと思います。

例えばランダムなNFTを定期的に配布したいといったものですかね

VRFの使い方

今回はSubscriptionsを使用した使用方法を解説します。

まずこちらからSubscription Accountを作成します。

Create Subscriptionを押下する

再度CreateSubscriptionを押下するとmetamaskで署名を求められるため順次対応していく

Chainlinkの呼出しにはLINKというトークンが必要です

VRFではRequestを送るたびにここにDepositしているLINKトークンが消費される仕組みになっています。

テスト用のLINKトークンはこちらで取得できます。

最後にConsumerの登録が必要なのですが、こちらは実際にChainlinkVRFを呼び出すコントラクトを追加する必要があるのでいったんスキップします。

デプロイするコントラクトのサンプルを以下に示します。(こちらはChainlink公式にあるサンプルコードです。) Remixなどでテストネットにデプロイしてみてください。

またそれぞれネットワーク依存のパラメータはこちらに明記されているので使用するテストネットに合わせて書き換えてください。(今回はehtereum seoiliaを使用しています)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import {VRFCoordinatorV2Interface} from "@chainlink/contracts@1.0.0/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol";
import {VRFConsumerBaseV2} from "@chainlink/contracts@1.0.0/src/v0.8/vrf/VRFConsumerBaseV2.sol";

/**
 * @notice A Chainlink VRF consumer which uses randomness to mimic the rolling
 * of a 20 sided dice
 */

/**
 * Request testnet LINK and ETH here: <https://faucets.chain.link/>
 * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: <https://docs.chain.link/docs/link-token-contracts/>
 */

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */

contract VRFD20 is VRFConsumerBaseV2 {
    uint256 private constant ROLL_IN_PROGRESS = 42;

    VRFCoordinatorV2Interface COORDINATOR;

    // Your subscription ID.
    uint64 s_subscriptionId;

    // Sepolia coordinator. For other networks,
    // see <https://docs.chain.link/docs/vrf-contracts/#configurations>
    address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;

    // The gas lane to use, which specifies the maximum gas price to bump to.
    // For a list of available gas lanes on each network,
    // see <https://docs.chain.link/docs/vrf-contracts/#configurations>
    bytes32 s_keyHash =
        0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;

    // Depends on the number of requested values that you want sent to the
    // fulfillRandomWords() function. Storing each word costs about 20,000 gas,
    // so 40,000 is a safe default for this example contract. Test and adjust
    // this limit based on the network that you select, the size of the request,
    // and the processing of the callback request in the fulfillRandomWords()
    // function.
    uint32 callbackGasLimit = 40000;

    // The default is 3, but you can set this higher.
    uint16 requestConfirmations = 3;

    // For this example, retrieve 1 random value in one request.
    // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
    uint32 numWords = 1;
    address s_owner;

    // map rollers to requestIds
    mapping(uint256 => address) private s_rollers;
    // map vrf results to rollers
    mapping(address => uint256) private s_results;

    event DiceRolled(uint256 indexed requestId, address indexed roller);
    event DiceLanded(uint256 indexed requestId, uint256 indexed result);

    /**
     * @notice Constructor inherits VRFConsumerBaseV2
     *
     * @dev NETWORK: Sepolia
     *
     * @param subscriptionId subscription id that this consumer contract can use
     */
    constructor(uint64 subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_owner = msg.sender;
        s_subscriptionId = subscriptionId;
    }

    /**
     * @notice Requests randomness
     * @dev Warning: if the VRF response is delayed, avoid calling requestRandomness repeatedly
     * as that would give miners/VRF operators latitude about which VRF response arrives first.
     * @dev You must review your implementation details with extreme care.
     *
     * @param roller address of the roller
     */
    function rollDice(
        address roller
    ) public onlyOwner returns (uint256 requestId) {
        require(s_results[roller] == 0, "Already rolled");
        // Will revert if subscription is not set and funded.
        requestId = COORDINATOR.requestRandomWords(
            s_keyHash,
            s_subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );

        s_rollers[requestId] = roller;
        s_results[roller] = ROLL_IN_PROGRESS;
        emit DiceRolled(requestId, roller);
    }

    /**
     * @notice Callback function used by VRF Coordinator to return the random number to this contract.
     *
     * @dev Some action on the contract state should be taken here, like storing the result.
     * @dev WARNING: take care to avoid having multiple VRF requests in flight if their order of arrival would result
     * in contract states with different outcomes. Otherwise miners or the VRF operator would could take advantage
     * by controlling the order.
     * @dev The VRF Coordinator will only send this function verified responses, and the parent VRFConsumerBaseV2
     * contract ensures that this method only receives randomness from the designated VRFCoordinator.
     *
     * @param requestId uint256
     * @param randomWords  uint256[] The random result returned by the oracle.
     */
    function fulfillRandomWords(
        uint256 requestId,
        uint256[] memory randomWords
    ) internal override {
        uint256 d20Value = (randomWords[0] % 20) + 1;
        s_results[s_rollers[requestId]] = d20Value;
        emit DiceLanded(requestId, d20Value);
    }

    /**
     * @notice Get the house assigned to the player once the address has rolled
     * @param player address
     * @return house as a string
     */
    function house(address player) public view returns (string memory) {
        require(s_results[player] != 0, "Dice not rolled");
        require(s_results[player] != ROLL_IN_PROGRESS, "Roll in progress");
        return getHouseName(s_results[player]);
    }

    /**
     * @notice Get the house name from the id
     * @param id uint256
     * @return house name string
     */
    function getHouseName(uint256 id) private pure returns (string memory) {
        string[20] memory houseNames = [
            "Targaryen",
            "Lannister",
            "Stark",
            "Tyrell",
            "Baratheon",
            "Martell",
            "Tully",
            "Bolton",
            "Greyjoy",
            "Arryn",
            "Frey",
            "Mormont",
            "Tarley",
            "Dayne",
            "Umber",
            "Valeryon",
            "Manderly",
            "Clegane",
            "Glover",
            "Karstark"
        ];
        return houseNames[id - 1];
    }

    modifier onlyOwner() {
        require(msg.sender == s_owner);
        _;
    }
}

ソースコードの解説

  • rollDice: Chainlink VRFにランダム性の要求を出す。
  • fulfillRandomWords: オラクルが結果を送り返すために使用する関数
  • house: EOAアドレスに割り当てられた家の名前を確認する

デプロイの際にコンストラクタでSubscriptionIDというものが必要になります。

こちらは先ほど作成したSubscriptionIDを指定してください。

デプロイしたら先ほどのChainlinkVRFの画面に戻りConsumerを追加します。

Add Consumerを押下してください。

こちらでもMetamaskの署名を求められるので署名していきます。

ここで追加したConsumerからしかChainlinkVRFを呼び出せません。

Consumerが登録されたらランダムな値を取得してしてみます。

Walletのアドレスを引数に持たせて**rollDice** を実行します。

rollDice を実行するとChainlinkのSubscriptionにRequestが来ることを確認することができ、これが取り込まれるとランダム値が生成されて、addressに家の名前が割り当てられます。

割り当てられた結果はhouse関数で確認できます。

今回はランダム値を取得するためのChainlinkVRFの実装方法を紹介しました。

LINKトークンの管理をしないといけないのは少々厄介ですが、ランダム値は比較的使用する機会が多い機能なので上記のように簡単に利用できるのはうれしいですね。

参考

chainlink公式

サポートされているネットワークに関する情報

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です