【问题标题】:How to locally unit-test Chainlink's Verifiable Random Function?如何在本地对 Chainlink 的可验证随机函数进行单元测试?
【发布时间】:2021-09-07 14:11:22
【问题描述】:

上下文

在尝试设置用于测试 this Chainlink VRF random number contract 的基本自托管单元测试环境(和 CI)时,我在如何在本地模拟任何相关区块链/测试网时遇到了一些困难。

例如,我找到了this repository that tests Chainlinks VRF。但是,对于默认部署,它建议/需要免费的KOVAN_RPC_URL,例如来自 Infura 的网站,甚至对于“本地部署”,它建议/需要来自例如的免费 MAINNET_RPC_URL Alchemy 的网站。

尝试/基线

我采用了来自 waffle 框架的单元测试环境,描述为:

文件结构

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

文件内容

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

RandomNumberConsumer.sol 文件内容已在 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"
}

yarn.lock 文件内容有点大,而且是自动生成的,所以你可以找到它on the Waffle framework repository。同样,package.json 可以在同一存储库中找到 here

命令

也可以简单地克隆具有指定文件结构here 的 repo,并使用以下命令运行测试:

git clone git@github.com:a-t-2/chainlink.git
git clone git@github.com: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

测试输出

这将测试AmIRichAlready.sol 文件并输出:

  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)

问题

如果提供了足够的“gas”,我需要哪些文件、文件结构和命令来自动测试getRandomNumber() 合约是否返回整数,否则返回错误?

【问题讨论】:

    标签: unit-testing random solidity chainlink


    【解决方案1】:

    我也遇到过这个问题,通过 mocking 解决了。这是我的 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;
        }
    }
    

    您可以在我的repository 中查看测试示例

    【讨论】:

      【解决方案2】:

      要在本地进行测试,您需要使用mocks,它可以模拟拥有一个预言机网络。因为您在本地工作,所以 Chainlink 节点不知道您的本地区块链,因此您实际上无法执行正确的 VRF 请求。请注意,您可以尝试部署本地 Chainlink 节点和本地区块链并让它们交谈,但它还没有完全支持,因此您可能会得到不同的结果。无论如何,根据您链接的hardhat starter kit,您可以在 hardhat.config.js 文件中将 defaultNetwork 设置为“hardhat”,然后在部署和运行集成测试(yarn test-integration)时,它将使用mocks 模拟 VRF 节点,并测试随机数的请求。查看测试here,模拟合约和链接令牌被部署here

      【讨论】:

      • chainlink 文档也提到了主网分叉,你成功将它用于 VRF 了吗?您是否设法使用 VRF V2 编写本地单元测试?
      • @ClementWalter 我很确定主网分叉不适用于测试 VRF(v1 或 v2)。您应该改用本地模拟。
      • 所以实际上我已经能够使用 rinkeby 分叉到最新的块,以避免通过在本地部署然后转到 vrf v2 dashbord 将我的本地合同地址添加为订阅者然后运行来为协调器使用模拟用分叉测试。虽然这并不理想
      猜你喜欢
      • 1970-01-01
      • 2021-01-16
      • 2014-11-23
      • 2016-08-24
      • 1970-01-01
      • 1970-01-01
      • 2010-09-10
      • 2014-02-08
      • 2012-07-18
      相关资源
      最近更新 更多