mardi 7 septembre 2021

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

Context

While trying to set up a basic self-hosted unit testing environment (and CI) that tests this Chainlink VRF rolldice 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
   |____ChainlinkVRF_rolldice.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 ChainlinkVRF_rolldice.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 wallet: Wallet;

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

  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"
  }
}

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 https://github.com/a-t-2/sol-testedv1.git
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
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)


  3 passing (4s)

Question

Which set of files, file structure and commands do I need to automatically test whether the rolldice contract returns an integer in range 1 to 6 if sufficient "gas" is provided, and an error otherwise?




Aucun commentaire:

Enregistrer un commentaire