How to locally unit-test Chainlink's Verifiable Random Function?
Asked Answered
D

2

10

Context

While trying to set up a basic self-hosted unit testing environment (and CI) that tests this Chainlink VRF random number contract, I am experiencing slight difficulties in how to simulate any relevant blockchains/testnets locally.

For example, I found this repository that tests Chainlinks VRF. However, for default deployment it suggests/requires a free KOVAN_RPC_URL e.g. from Infura's site and even for "local deployment" it suggests/requires a free MAINNET_RPC_URL from e.g. Alchemy's site.

Attempt/baseline

I adopted a unit test environment from the waffle framework which is described as:

Filestructure

src____AmIRichAlready.sol
   |____RandomNumberConsumer.sol
   |
test____AmIRichAlready.test.ts
   |____mocha.opts
package.json
tsconfig.json
waffle.json
yarn.lock

Filecontents

AmIRichAlready.sol

pragma solidity ^0.6.2;

interface IERC20 {
    function balanceOf(address account) external view returns (uint256);
}

contract AmIRichAlready {
    IERC20 private tokenContract;
    uint public richness = 1000000 * 10 ** 18;

    constructor (IERC20 _tokenContract) public {
        tokenContract = _tokenContract;
    }

    function check() public view returns (bool) {
        uint balance = tokenContract.balanceOf(msg.sender);
        return balance > richness;
    }

    // IS THIS NEEDED???
    function setRichness(uint256 _richness) public {
      richness = _richness;
    }
}

The RandomNumberConsumer.sol filecontent is already on stackexange over here.

AmIRichAlready.test.ts

import {expect, use} from 'chai';
import {Contract, utils, Wallet} from 'ethers';
import {deployContract, deployMockContract, MockProvider, solidity} from 'ethereum-waffle';

import IERC20 from '../build/IERC20.json';
import AmIRichAlready from '../build/AmIRichAlready.json';

use(solidity);

describe('Am I Rich Already', () => {
  let mockERC20: Contract;
  let contract: Contract;
  let vrfContract: Contract;
  let wallet: Wallet;

  beforeEach(async () => {
    [wallet] = new MockProvider().getWallets();
    mockERC20 = await deployMockContract(wallet, IERC20.abi);
    contract = await deployContract(wallet, AmIRichAlready, [mockERC20.address]);
    vrfContract = await deployContract(wallet, RandomNumberConsumer);
  });

  it('checks if contract called balanceOf with certain wallet on the ERC20 token', async () => {
    await mockERC20.mock.balanceOf
      .withArgs(wallet.address)
      .returns(utils.parseEther('999999'));
    await contract.check();
    expect('balanceOf').to.be.calledOnContractWith(mockERC20, [wallet.address]);
  });

  it('returns false if the wallet has less than 1000000 coins', async () => {
    await mockERC20.mock.balanceOf
      .withArgs(wallet.address)
      .returns(utils.parseEther('999999'));
    expect(await contract.check()).to.be.equal(false);
  });

  it('returns true if the wallet has at least 1000000 coins', async () => {
    await mockERC20.mock.balanceOf
      .withArgs(wallet.address)
      .returns(utils.parseEther('1000000'));
    expect(await contract.check()).to.be.equal(false);
  });
});

mocha.opts

-r ts-node/register/transpile-only
--timeout 50000
--no-warnings
test/**/*.test.{js,ts}

package.json

{
  "name": "example-dynamic-mocking-and-testing-calls",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "export NODE_ENV=test && mocha",
    "build": "waffle",
    "lint": "eslint '{src,test}/**/*.ts'",
    "lint:fix": "eslint --fix '{src,test}/**/*.ts'"
  },
  "devDependencies": {
    "@openzeppelin/contracts": "^4.3.1",
    "@types/chai": "^4.2.3",
    "@types/mocha": "^5.2.7",
    "@typescript-eslint/eslint-plugin": "^2.30.0",
    "@typescript-eslint/parser": "^2.30.0",
    "chai": "^4.3.4",
    "eslint": "^6.8.0",
    "eslint-plugin-import": "^2.20.2",
    "ethereum-waffle": "^3.4.0",
    "ethers": "^5.0.17",
    "mocha": "^7.2.0",
    "ts-node": "^8.9.1",
    "typescript": "^3.8.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "declaration": true,
    "esModuleInterop": true,
    "lib": [
      "ES2018"
    ],
    "module": "CommonJS",
    "moduleResolution": "node",
    "outDir": "dist",
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2018"
  }

  // custom test in vrfContract
  it('Tests if a random number is returned', async () => {
    expect(await vrfContract.getRandomNumber()).to.be.equal(7);
  });
}

waffle.json

{
  "compilerType": "solcjs",
  "compilerVersion": "0.6.2",
  "sourceDirectory": "./src",
  "outputDirectory": "./build"
}

The yarn.lock file content is a bit large, and it's auto-generated, so you can find it on the Waffle framework repository. Similarly, the package.json can be found here, in the same repository.

Commands

One can also simply clone the repo with the specified filestructure here, and run the tests with the following commands:

git clone [email protected]:a-t-2/chainlink.git
git clone [email protected]:a-t-2/test_vrf3.git
cd test_vrf3
sudo apt install npm
npm install
npm audit fix
npm install --save-dev ethereum-waffle
npm install @openzeppelin/contracts -D
npm i chai -D
npm i mocha -D
rm -r build
npx waffle
npx mocha
npm test

Test Output

This will test the AmIRichAlready.sol file and output:

  Am I Rich Already
    ✓ checks if contract called balanceOf with certain wallet on the ERC20 token (249ms)
    ✓ returns false if the wallet has less than 1000000 coins (190ms)
    ✓ returns true if the wallet has at least 1000000 coins (159ms)
    Tests if a random number is returned:
     Error: cannot estimate gas; transaction may fail or may require manual gas limit (error={"name":"RuntimeError","results":{"0x0a0b028de6cf6e8446853a300061305501136cefa5f5eb3e96afd95dbd73dd92":{"error":"revert","program_counter":609,"return":"0x"}},"hashes":["0x0a0b028de6cf6e8446853a300061305501136cefa5f5eb3e96afd95dbd73dd92"],"message":"VM Exception while processing transaction: revert"}, tx={"data":"0xdbdff2c1","to":{},"from":"0x17ec8597ff92C3F44523bDc65BF0f1bE632917ff","gasPrice":{"type":"BigNumber","hex":"0x77359400"},"type":0,"nonce":{},"gasLimit":{},"chainId":{}}, code=UNPREDICTABLE_GAS_LIMIT, version=abstract-signer/5.4.1)
      at Logger.makeError (node_modules/@ethersproject/logger/src.ts/index.ts:225:28)
      at Logger.throwError (node_modules/@ethersproject/logger/src.ts/index.ts:237:20)
      at /home/name/git/trucol/tested/new_test/test_vrf3/node_modules/@ethersproject/abstract-signer/src.ts/index.ts:301:31
      at process._tickCallback (internal/process/next_tick.js:68:7)



  3 passing (4s)

Question

Which set of files, file structure and commands do I need to automatically test whether the getRandomNumber() contract returns an integer if sufficient "gas" is provided, and an error otherwise?

Deirdredeism answered 7/9, 2021 at 14:11 Comment(0)
H
10

I faced this problem too and solved by mocking. Here is my MockVRFCoordinator:

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

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract MockVRFCoordinator {
    uint256 internal counter = 0;

    function requestRandomWords(
        bytes32,
        uint64,
        uint16,
        uint32,
        uint32
    ) external returns (uint256 requestId) {
        VRFConsumerBaseV2 consumer = VRFConsumerBaseV2(msg.sender);
        uint256[] memory randomWords = new uint256[](1);
        randomWords[0] = counter;
        consumer.rawFulfillRandomWords(requestId, randomWords);
        counter += 1;
    }
}

You can see test example in my repository

Hystero answered 8/3, 2022 at 4:12 Comment(0)
U
4

to test locally you need to make use of mocks which can simulate having an oracle network. Because you're working locally, a Chainlink node doesn't know about your local blockchain, so you can't actually do proper VRF requests. Note you can try deploy a local Chainlink node and a local blockchain and have them talk, but it isn't fully supported yet so you may get mixed results. Anyway, as per the hardhat starter kit that you linked, you can set the defaultNetwork to be 'hardhat' in the hardhat.config.js file, then when you deploy and run the integration tests (yarn test-integration), it will use mocks to mock up the VRF node, and to test the requesting of a random number. See the test here, and the mock contracts and linktoken get deployed here

Unite answered 9/9, 2021 at 4:35 Comment(6)
the chainlink doc mentions also mainnet forking, have you succeed using it for VRF? Have you managed to write local unit-test with VRF V2?Moorhead
@Moorhead I'm pretty sure mainnet forking does not work for testing VRF (v1 or v2). You should instead use local mocks.Noisette
so actually I've been able to use rinkeby forking up to latest block to avoid using mocks for the coordinator by deploying locally then going to the vrf v2 dashbord to add my local contract address as subscriber then running tests with forking. This is not ideal thoughMoorhead
@Moorhead would you care to share your forking procedure? I have been unable to fork rinkeby for VRF integration testing.Byandby
I have eventually written a whole tuto for this mirror.xyz/clemlaflemme.eth/… @ByandbyMoorhead
Ah great, so that's basically the mockup approach, right? Does that give you any confidence on the whole system testing?Byandby

© 2022 - 2024 — McMap. All rights reserved.