# How to unit-test with Chainlink VRF V2

By [Clemlaflemme](https://paragraph.com/@clemlaflemme) · 2022-03-09

---

How to locally unit-test a contract using Chainlink VRF V2
----------------------------------------------------------

While it is quite straightforward to use the Chainlink VRF V2 oracle, the [Request & Receive Data cycle](https://docs.chain.link/docs/request-and-receive-data/) is a bit less easy to use on a local network (e.g. a hardhat node for testing) where there is no Chainlink node listening to the calls.

This article aims at giving a step-by-step guide to a working solution for unit-testing a contract using the new Chainlink VRF V2 oracle (Chainlink actually provides an example for the VRF V1 version, see [the hardhat starter kit](https://github.com/smartcontractkit/hardhat-starter-kit/blob/main/test/unit/RandomNumberConsumer_unit_test.js))

Using the Chainlink VRF V2 oracle
---------------------------------

### Setup

First of all, we detail here how to use the Chainlink VRF V2 oracle. These steps are somehow described in the main documentation page of the [Chainlink VRF V2 oracle](https://docs.chain.link/docs/get-a-random-number/#analyzing-the-contract).

In Chainlink's vocabulary:

*   a random number is called a "random word"
    
*   the contract is a _consumer_ of the oracle, i.e. of the random number provider
    
*   this _consumer_ requires to ask a _coordinator_ to provide a random number
    
*   the _consumer_ can do so by:
    
    *   subscribing to the _coordinator_
        
    *   funding its subscription with some `LINK` tokens
        
    *   calling the _coordinator_ whenever they wants to get a new random number
        
    *   implementing a callback function that will be called by the _coordinator_
        

To interact with Chainlink's contracts, we first add their package to our project:

    npm install @chainlink/contracts
    

Then, let's create a new contract:

    pragma solidity ^0.8.12;
    
    import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
    
    contract MyContract is VRFConsumerBaseV2 {
        constructor(address vrfCoordinator) VRFConsumerBaseV2(vrfCoordinator) {}
    }
    

As mentioned earlier, the contract is a _consumer_ of the oracle and hence we inherit from the `VRFConsumerBaseV2` contract provided by Chainlink. We see that the `VRFConsumerBaseV2` takes as input the address of the _coordinator_. For `rinkeby` and `mainnet`, this address is provided by Chainlink itself [here](https://docs.chain.link/docs/vrf-contracts/).

In order to call the _coordinator_, `MyContract` needs to directly make a call to it. Hence we add:

    pragma solidity ^0.8.12;
    
    import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
    import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
    
    contract MyContract is VRFConsumerBaseV2 {
      
        VRFCoordinatorV2Interface COORDINATOR;
        
        constructor(address vrfCoordinator) VRFConsumerBaseV2(vrfCoordinator) {
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        }
    }
    

As we said earlier, `MyContract` has to subscribe to the Chainlink's coordinator. Otherwise it would not be possible for Chainlink to differentiate between different consumers. The subscription can be done directly on the [Chainlink's website](https://vrf.chain.link/) but we will make it programmatically to be able to subscribe to our own coordinator when unit-testing locally. This can be done as follows:

    pragma solidity ^0.8.12;
    
    import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
    import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
    
    contract MyContract is VRFConsumerBaseV2 {
      
        VRFCoordinatorV2Interface COORDINATOR;
        uint64 subscriptionId;
        
        constructor(address vrfCoordinator) VRFConsumerBaseV2(vrfCoordinator) {
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
            subscriptionId = COORDINATOR.createSubscription();
            COORDINATOR.addConsumer(subscriptionId, address(this));
        }
      
        function cancelSubscription() external {
            COORDINATOR.cancelSubscription(subscriptionId, msg.sender);
        }  
    }
    

The last step is to fund the _consumer_ with some `LINK` tokens to pay the oracle:

    pragma solidity ^0.8.12;
    
    import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
    import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
    import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
    
    contract MyContract is VRFConsumerBaseV2 {
      
        VRFCoordinatorV2Interface COORDINATOR;
        LinkTokenInterface LINKTOKEN;
        uint64 subscriptionId;
        
        constructor(address vrfCoordinator, address link) VRFConsumerBaseV2(vrfCoordinator) {
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
            subscriptionId = COORDINATOR.createSubscription();
            COORDINATOR.addConsumer(subscriptionId, address(this));
            LINKTOKEN = LinkTokenInterface(link);
        }
      
        function cancelSubscription() external {
            COORDINATOR.cancelSubscription(subscriptionId, msg.sender);
        }
      
        function fund(uint96 amount) public {
            LINKTOKEN.transferAndCall(
                address(COORDINATOR),
                amount,
                abi.encode(subscriptionId)
            );
        }
    }
    

Note that the `fund` function is outside of the constructor because we want to be able to call it at any time to refill our contract.

### Requesting a random number

When one wants to request a random number, one then only needs to call the coordinator's `requestRandomWords` function. For example, let's implement a `randomnessIsRequestedHere` function:

    pragma solidity ^0.8.12;
    
    import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
    import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
    import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
    
    contract MyContract is VRFConsumerBaseV2 {
      
        VRFCoordinatorV2Interface COORDINATOR;
        LinkTokenInterface LINKTOKEN;
        uint64 subscriptionId;
        
        constructor(address vrfCoordinator, address link) VRFConsumerBaseV2(vrfCoordinator) {
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
            subscriptionId = COORDINATOR.createSubscription();
            COORDINATOR.addConsumer(subscriptionId, address(this));
            LINKTOKEN = LinkTokenInterface(link);
        }
      
        function cancelSubscription() external {
            COORDINATOR.cancelSubscription(subscriptionId, msg.sender);
        }
      
        function fund(uint96 amount) public {
            LINKTOKEN.transferAndCall(
                address(COORDINATOR),
                amount,
                abi.encode(subscriptionId)
            );
        }
      
        function randomnessIsRequestedHere() public {
            uint256 requestId = COORDINATOR.requestRandomWords(
                keyHash,
                subscriptionId,
                minimumRequestConfirmations,
                callbackGasLimit,
                numWords
            );
        }
    }
    

The `requestRandomWords` function is documented [here](https://docs.chain.link/docs/get-a-random-number/#create-and-fund-a-subscription). The parameter names almost speak for themselves. The `keyHash` is a parameter to give a priority level to the callback transaction. What is this callback transaction? It is the transaction that will be executed by the _coordinator_ when the random number is ready. Actually, when inheriting from `VRFConsumerBaseV2`, `MyContract` has to implement the `fulfillRandomWords` function. As mentioned in the contract's documentation:

      // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF
      // proof. rawFulfillRandomness then calls fulfillRandomness, after validating
      // the origin of the call
      function rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) external {
        if (msg.sender != vrfCoordinator) {
          revert OnlyCoordinatorCanFulfill(msg.sender, vrfCoordinator);
        }
        fulfillRandomWords(requestId, randomWords);
      }
    

This is what makes the `fulfillRandomWords` hard to test locally as we will see later on. In any case, for now, a possible working implementation is:

    pragma solidity ^0.8.12;
    
    import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
    import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
    import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
    
    contract MyContract is VRFConsumerBaseV2 {
      
        VRFCoordinatorV2Interface COORDINATOR;
        LinkTokenInterface LINKTOKEN;
        uint64 subscriptionId;
        
        constructor(address vrfCoordinator, address link) VRFConsumerBaseV2(vrfCoordinator) {
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
            subscriptionId = COORDINATOR.createSubscription();
            COORDINATOR.addConsumer(subscriptionId, address(this));
            LINKTOKEN = LinkTokenInterface(link);
        }
      
        function cancelSubscription() external {
            COORDINATOR.cancelSubscription(subscriptionId, msg.sender);
        }
      
        function fund(uint96 amount) public {
            LINKTOKEN.transferAndCall(
                address(COORDINATOR),
                amount,
                abi.encode(subscriptionId)
            );
        }
      
        function randomnessIsRequestedHere() public {
            uint256 requestId = COORDINATOR.requestRandomWords(
                keyHash,
                subscriptionId,
                minimumRequestConfirmations,
                callbackGasLimit,
                numWords
            );
        }
      
        function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
            internal
            override
        {
            // do something with the random words
            // use requestId to identify the request
        }  
    }
    

Testing the contract
--------------------

For this section, with specifically use [hardhat](https://hardhat.org/) and the [hardhat-deploy plugin](https://github.com/wighawag/hardhat-deploy).

If we want to unit-test locally this contract, we need to be able to call both the `randomnessIsRequestedHere` and the `fulfillRandomWords` functions. In other words, we need to have a working local `COORDINATOR` that will:

*   returns a valid `requestId`
    
*   be used to call the `MyContract.rawFulfillRandomness` function that will in turns call the `MyContract.fulfillRandomWords` function
    

Fortunately, the `@chainlink/contracts` library provides a `VRFCoordinatorV2TestHelper` class that can be used to create a local `COORDINATOR`. We need:

*   to deploy it
    
*   to update its config
    
*   to fund it
    

This later part requires to write a new contract because the `@chainlink`'s contract does not implement the `receive` function.

    pragma solidity ^0.8.12;
    
    import {VRFCoordinatorV2TestHelper as Helper} from "@chainlink/contracts/src/v0.8/tests/VRFCoordinatorV2TestHelper.sol";
    
    contract VRFCoordinatorV2TestHelper is Helper {
        receive() external payable {}
    
        constructor(
            address link,
            address blockhashStore,
            address linkEthFeed
        ) Helper(link, blockhashStore, linkEthFeed) {}
    }
    

To ease the local deployment, it is possible to use `rinkeby` forking. This will make it possible to avoid deploying also the `LinkToken` contract and the `Link/Eth` contract feed, see [hardhat doc](https://hardhat.org/hardhat-network/guides/mainnet-forking.html):

    // hardhat.config.ts
    const config: HardhatUserConfig = {
      // ...
      networks: {
        hardhat: {
          // ...
          forking: {
            url: "provider-url",
          }
          // ...
        },
      },
      // ...
    }
    

Using the `hardhat-deploy` plugin, it can then look like this:

    const vrfTx = await deploy("VRFCoordinatorV2TestHelper", {
      from: deployer,
      log: true,
      args: [linkAddress, blockHashStore, linkEthFeed],
      contract:
        "contracts/test/VRFCoordinatorV2TestHelper.sol:VRFCoordinatorV2TestHelper",
    });
    const vrfCoordinatorAddress = vrfTx.address;
    await execute(
      "VRFCoordinatorV2TestHelper",
      { from: deployer },
      "setConfig",
      3,
      2500000,
      86400,
      33285,
      "60000000000000000",
      {
        fulfillmentFlatFeeLinkPPMTier1: 250000,
        fulfillmentFlatFeeLinkPPMTier2: 250000,
        fulfillmentFlatFeeLinkPPMTier3: 250000,
        fulfillmentFlatFeeLinkPPMTier4: 250000,
        fulfillmentFlatFeeLinkPPMTier5: 250000,
        reqsForTier2: 0,
        reqsForTier3: 0,
        reqsForTier4: 0,
        reqsForTier5: 0,
      }
    );
    

*   the `blockHashStore` can be set to `ethers.constants.AddressZero`
    
*   the `linkAddress` and the `linkEthFeed` are the one for rinkeby as described [here](https://docs.chain.link/docs/vrf-contracts/) and [here](https://docs.chain.link/docs/ethereum-addresses/)
    
*   the parameters of the `setConfig` function are the one found using `getConfig` of the deployed coordinator on rinkeby, for example [on etherscan](https://rinkeby.etherscan.io/address/0x6168499c0cFfCaCD319c818142124B7A15E857ab#readContract)
    

The last part of the deployment process is to fund the subscription. Using the rinkeby forking, you will automatically have available locally the `LINK` token that you will have requested in the [chainlink faucet](https://faucets.chain.link/). So you just need to send them to the contract:

    const LinkToken = await ethers.getContractAt(
        [
          "function balanceOf(address owner) view returns (uint256 balance)",
          "function transferFrom(address from, address to, uint256 value) returns (bool success)",
          "function approve(address _spender, uint256 _value) returns (bool)",
        ],
        linkAddress,
        deployer
    );
    const deployerBalance = await LinkToken.balanceOf(deployer);
    // deployerBalance is the balance of your deployer account on rinkeby
    await LinkToken.approve(deployer, deployerBalance, {
        from: deployer,
    });
    // this sounds weired but I could not make it work without it
    const txTransfer = await LinkToken.transferFrom(
        deployer,
        MyContract.address,
        deployerBalance
    );
    await txTransfer.wait();
    await execute(
        "MyContract",
        { from: deployer },
        "fund",
        deployerBalance
    );
    

And that's it! You can now call `MyContract.randomnessIsRequestedHere` and `MyContract.rawFulfillRandomness` locally in your unit tests. Wait. Not exactly!

For the coordinator to be able to call `MyContract.rawFulfillRandomness`, they need to have some ETH to pay the transaction fees with… and hardhat needs to understands that it’s not the default signer calling the contracts. Using the `createFixture`\` of the `hardhat-deploy` plugin, you can do something like this to send 1 ETH to the coordinator:

    const vrfFixture = deployments.createFixture(async ({}) => {
      await network.provider.request({
        method: "hardhat_impersonateAccount",
        params: [VRFCoordinator.address],
      });
      await (
        await ethers.getSigner(users[0].address)
      ).sendTransaction({
        to: VRFCoordinator.address,
        value: ethers.utils.parseEther("1"),
      });
    });
    

And in before each test where you call on behalf on the coordinator, just `await vrfFixture()` beforehand.

I hope this helps you to get started with [Chainlink VRF V2](https://chain.link/). If you have any questions, please reach to me on [Twitter](https://twitter.com/ClementWalter) or [discord](https://discord.gg/5tmuenJwDh).

---

*Originally published on [Clemlaflemme](https://paragraph.com/@clemlaflemme/how-to-unit-test-with-chainlink-vrf-v2)*
