From 4fbaa8b6c9209c85b4e1785171dd20b6e454d773 Mon Sep 17 00:00:00 2001 From: Petia Latinova Date: Thu, 3 Nov 2022 10:38:25 +0200 Subject: [PATCH] feat: upgrade contracts to vrf v2 (#128) * use vrf v2 direct funding for RandomNumberConsumer * use vrf v2 subscription for RandomSVG * change auto fund amount to 5 LINK Co-authored-by: ivaylonikolov7 Co-authored-by: imollov --- README.md | 32 +- .../frontend/components/vrf/RandomNumber.tsx | 9 +- .../frontend/contracts/hardhat_contracts.json | 1030 ++++++++++++++--- .../contracts/RandomNumberConsumer.sol | 49 +- packages/hardhat/contracts/RandomSVG.sol | 124 +- .../contracts/mocks/VRFCoordinatorMock.sol | 4 - .../contracts/mocks/VRFCoordinatorV2Mock.sol | 4 + .../hardhat/contracts/mocks/VRFV2Wrapper.sol | 4 + packages/hardhat/deploy/01_Deploy_Mocks.ts | 49 +- .../deploy/04_Deploy_RandomNumberConsumer.ts | 16 +- .../hardhat/deploy/05_Deploy_RandomSVG.ts | 101 +- packages/hardhat/deploy/07_Setup_Contracts.ts | 16 +- packages/hardhat/hardhat.config.ts | 9 +- packages/hardhat/helper-hardhat-config.ts | 29 +- packages/hardhat/test/unit/RandomSVG.test.ts | 61 +- 15 files changed, 1193 insertions(+), 344 deletions(-) delete mode 100644 packages/hardhat/contracts/mocks/VRFCoordinatorMock.sol create mode 100644 packages/hardhat/contracts/mocks/VRFCoordinatorV2Mock.sol create mode 100644 packages/hardhat/contracts/mocks/VRFV2Wrapper.sol diff --git a/README.md b/README.md index fd93576..2290135 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Built with: In order to use the frontend portion of the demo application you will need -- A crypto wallet such as [Metamask](https://metamask.io/) or [Coinbase Wallet](https://www.coinbase.com/wallet) +- A crypto wallet such as [Metamask](https://metamask.io/) or [Coinbase Wallet](https://www.coinbase.com/wallet) - Test $LINK for the relevant testnet. You can get some at the [Chainlink Faucets](https://faucets.chain.link/) page. - Test $ETH to pay for gas costs. You can get some at the [Chainlink Faucets](https://faucets.chain.link/) page. @@ -97,6 +97,10 @@ To deploy on a public network: yarn deploy --network goerli ``` +Before deploying `RandomSVG` contract on a public network, an ID of a prefunded VRF subscription must be set in [`helper-hardhat-config.ts`](/packages/hardhat/helper-hardhat-config.ts). + +See how to [Create and Fund a Subscription](https://docs.chain.link/docs/vrf/v2/subscription/ui/). + ## Auto-Funding The Hardhat project will attempt to auto-fund any newly deployed contract that uses Any-API or VRF, which otherwise has to be done manually. @@ -105,7 +109,7 @@ The amount in LINK to send as part of this process can be modified in this [Hard | Parameter | Description | Default Value | | ---------- | :------------------------------------------------ | :------------ | -| fundAmount | Amount of LINK to transfer when funding contracts | 1 LINK | +| fundAmount | Amount of LINK to transfer when funding contracts | 5 LINK | If you wish to deploy the smart contracts without performing the auto-funding, run the following command when doing your deployment: @@ -170,22 +174,22 @@ Once the `deploy` command is executed on any network the contracts config will b #### Goerli -| Name | Address | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `PriceConsumerV3` | [0x870Ad4995160C65A31478Fb438Dca267D6053EAF](https://goerli.etherscan.io/address/0x870Ad4995160C65A31478Fb438Dca267D6053EAF) | -| `APIConsumer` | [0xe40D4f1fDf9f0312905bd938Dd396B9149e1F04b](https://goerli.etherscan.io/address/0xe40D4f1fDf9f0312905bd938Dd396B9149e1F04b) | -| `RandomNumberConsumer` | [0xF498E392f010b6513781418BAfC2F540690F3E13](https://goerli.etherscan.io/address/0xF498E392f010b6513781418BAfC2F540690F3E13) | -| `RandomSVG` | [0xF32b62E03cEeA101B30b0B90aD0038B97f635025](https://goerli.etherscan.io/address/0xF32b62E03cEeA101B30b0B90aD0038B97f635025) | +| Name | Address | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `PriceConsumerV3` | [0x46b73aca4AF8D060355beAb7f3C941B214ba0E1F](https://goerli.etherscan.io/address/0x46b73aca4AF8D060355beAb7f3C941B214ba0E1F) | +| `APIConsumer` | [0xe40D4f1fDf9f0312905bd938Dd396B9149e1F04b](https://goerli.etherscan.io/address/0xe40D4f1fDf9f0312905bd938Dd396B9149e1F04b) | +| `RandomNumberConsumer` | [0x35ea06342a82e091040CbF415cc899228DB4C936](https://goerli.etherscan.io/address/0x35ea06342a82e091040CbF415cc899228DB4C936) | +| `RandomSVG` | [0xa652548CDAb898d9d885896f464Fd4a07F353aBc](https://goerli.etherscan.io/address/0xa652548CDAb898d9d885896f464Fd4a07F353aBc) | #### Kovan (deprecated) -| Name | Address | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `PriceConsumerV3` | [0x01E2C7cA6D6A82D059287Cb0bC43a39Cd0ff4B00](https://kovan.etherscan.io/address/0x01E2C7cA6D6A82D059287Cb0bC43a39Cd0ff4B00) | -| `FeedRegistryConsumer` | [0xB9ebb63D4820c45a2Db09d71cefA24daBd047b50](https://kovan.etherscan.io/address/0xB9ebb63D4820c45a2Db09d71cefA24daBd047b50) | -| `APIConsumer` | [0x14005AB90bc520E20Ffd7815Cae64372abb6b04d](https://kovan.etherscan.io/address/0x14005AB90bc520E20Ffd7815Cae64372abb6b04d) | +| Name | Address | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `PriceConsumerV3` | [0x01E2C7cA6D6A82D059287Cb0bC43a39Cd0ff4B00](https://kovan.etherscan.io/address/0x01E2C7cA6D6A82D059287Cb0bC43a39Cd0ff4B00) | +| `FeedRegistryConsumer` | [0xB9ebb63D4820c45a2Db09d71cefA24daBd047b50](https://kovan.etherscan.io/address/0xB9ebb63D4820c45a2Db09d71cefA24daBd047b50) | +| `APIConsumer` | [0x14005AB90bc520E20Ffd7815Cae64372abb6b04d](https://kovan.etherscan.io/address/0x14005AB90bc520E20Ffd7815Cae64372abb6b04d) | | `RandomNumberConsumer` | [0xF9556187bf86823Cf0D7081625F97391642Fc242](https://kovan.etherscan.io/address/0xF9556187bf86823Cf0D7081625F97391642Fc242) | -| `RandomSVG` | [0xb4Bac68d9Fa99D2852E5dFb124be74de2E8c4F76](https://kovan.etherscan.io/address/0xb4Bac68d9Fa99D2852E5dFb124be74de2E8c4F76) | +| `RandomSVG` | [0xb4Bac68d9Fa99D2852E5dFb124be74de2E8c4F76](https://kovan.etherscan.io/address/0xb4Bac68d9Fa99D2852E5dFb124be74de2E8c4F76) | #### Rinkeby (deprecated) diff --git a/packages/frontend/components/vrf/RandomNumber.tsx b/packages/frontend/components/vrf/RandomNumber.tsx index 3c4f3c4..beacfac 100644 --- a/packages/frontend/components/vrf/RandomNumber.tsx +++ b/packages/frontend/components/vrf/RandomNumber.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { Text, Button, Code, Stack } from '@chakra-ui/react' import { useContractFunction, useEthers } from '@usedapp/core' +import { BigNumber } from 'ethers' import { getRequestStatus, getContractError } from '../../lib/utils' import { useContract } from '../../hooks/useContract' import { Error } from '../Error' @@ -10,7 +11,7 @@ import { RandomNumberConsumer } from '../../../types/typechain' export function RandomNumber(): JSX.Element { const { account, error } = useEthers() - const [requestId, setRequestId] = useState('') + const [requestId, setRequestId] = useState() const [randomNumber, setRandomNumber] = useState('') const randomNumberConsumer = useContract( @@ -20,7 +21,7 @@ export function RandomNumber(): JSX.Element { const { send, state, events } = useContractFunction( randomNumberConsumer, 'getRandomNumber', - { transactionName: 'Randomness Request' } + { transactionName: 'Randomness Request', gasLimitBufferPercentage: 250 } ) const requestRandomNumber = async () => { @@ -44,8 +45,8 @@ export function RandomNumber(): JSX.Element { useEffect(() => { if (randomNumberConsumer && requestId) { - randomNumberConsumer.on('FulfilledRandomness', (id: string) => { - if (requestId === id) { + randomNumberConsumer.on('FulfilledRandomness', (id: BigNumber) => { + if (requestId.eq(id)) { readRandomNumber() randomNumberConsumer.removeAllListeners() } diff --git a/packages/frontend/contracts/hardhat_contracts.json b/packages/frontend/contracts/hardhat_contracts.json index dba3ac6..ceaf9d3 100644 --- a/packages/frontend/contracts/hardhat_contracts.json +++ b/packages/frontend/contracts/hardhat_contracts.json @@ -232,7 +232,7 @@ ] }, "PriceConsumerV3": { - "address": "0x870Ad4995160C65A31478Fb438Dca267D6053EAF", + "address": "0x46b73aca4AF8D060355beAb7f3C941B214ba0E1F", "abi": [ { "inputs": [ @@ -261,13 +261,13 @@ ] }, "RandomNumberConsumer": { - "address": "0xF498E392f010b6513781418BAfC2F540690F3E13", + "address": "0x35ea06342a82e091040CbF415cc899228DB4C936", "abi": [ { "inputs": [ { "internalType": "address", - "name": "_vrfCoordinator", + "name": "_wrapperAddress", "type": "address" }, { @@ -276,14 +276,9 @@ "type": "address" }, { - "internalType": "bytes32", - "name": "_keyHash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "_fee", - "type": "uint256" + "internalType": "uint32", + "name": "_callbackGasLimit", + "type": "uint32" } ], "stateMutability": "nonpayable", @@ -294,9 +289,9 @@ "inputs": [ { "indexed": false, - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" } ], "name": "FulfilledRandomness", @@ -307,9 +302,9 @@ "inputs": [ { "indexed": false, - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" } ], "name": "RequestedRandomness", @@ -320,9 +315,9 @@ "name": "getRandomNumber", "outputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -343,18 +338,18 @@ }, { "inputs": [ - { - "internalType": "bytes32", - "name": "requestId", - "type": "bytes32" - }, { "internalType": "uint256", - "name": "randomness", + "name": "_requestId", "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "_randomWords", + "type": "uint256[]" } ], - "name": "rawFulfillRandomness", + "name": "rawFulfillRandomWords", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -369,34 +364,50 @@ ] }, "RandomSVG": { - "address": "0xF32b62E03cEeA101B30b0B90aD0038B97f635025", + "address": "0xa652548CDAb898d9d885896f464Fd4a07F353aBc", "abi": [ { "inputs": [ { "internalType": "address", - "name": "_VRFCoordinator", + "name": "_vrfCoordinatorV2", "type": "address" }, { - "internalType": "address", - "name": "_LinkToken", - "type": "address" + "internalType": "uint64", + "name": "_vrfSubscriptionId", + "type": "uint64" }, { "internalType": "bytes32", - "name": "_keyhash", + "name": "_vrfGasLane", "type": "bytes32" }, { - "internalType": "uint256", - "name": "_fee", - "type": "uint256" + "internalType": "uint32", + "name": "_vrfCallbackGasLimit", + "type": "uint32" } ], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { + "internalType": "address", + "name": "have", + "type": "address" + }, + { + "internalType": "address", + "name": "want", + "type": "address" + } + ], + "name": "OnlyCoordinatorCanFulfill", + "type": "error" + }, { "anonymous": false, "inputs": [ @@ -534,9 +545,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" }, { "indexed": true, @@ -607,13 +618,7 @@ { "inputs": [], "name": "create", - "outputs": [ - { - "internalType": "bytes32", - "name": "requestId", - "type": "bytes32" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -842,17 +847,17 @@ { "inputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" }, { - "internalType": "uint256", - "name": "randomness", - "type": "uint256" + "internalType": "uint256[]", + "name": "randomWords", + "type": "uint256[]" } ], - "name": "rawFulfillRandomness", + "name": "rawFulfillRandomWords", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -867,9 +872,9 @@ { "inputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "", - "type": "bytes32" + "type": "uint256" } ], "name": "requestIdToSender", @@ -886,9 +891,9 @@ { "inputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "", - "type": "bytes32" + "type": "uint256" } ], "name": "requestIdToTokenId", @@ -1140,7 +1145,7 @@ "chainId": "31337", "contracts": { "APIConsumer": { - "address": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508", "abi": [ { "inputs": [ @@ -1213,45 +1218,19 @@ { "indexed": true, "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - } - ], - "name": "OwnershipTransferRequested", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", + "name": "previousOwner", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "to", + "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, - { - "inputs": [], - "name": "acceptOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "data", @@ -1283,6 +1262,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isOwner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -1361,7 +1353,7 @@ "inputs": [ { "internalType": "address", - "name": "to", + "name": "newOwner", "type": "address" } ], @@ -1680,7 +1672,7 @@ ] }, "FeedRegistryConsumer": { - "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F", + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", "abi": [ { "inputs": [ @@ -2273,7 +2265,7 @@ ] }, "MockOracle": { - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F", "abi": [ { "inputs": [ @@ -2656,7 +2648,7 @@ ] }, "PriceConsumerV3": { - "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", "abi": [ { "inputs": [ @@ -2685,13 +2677,13 @@ ] }, "RandomNumberConsumer": { - "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", "abi": [ { "inputs": [ { "internalType": "address", - "name": "_vrfCoordinator", + "name": "_wrapperAddress", "type": "address" }, { @@ -2700,14 +2692,9 @@ "type": "address" }, { - "internalType": "bytes32", - "name": "_keyHash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "_fee", - "type": "uint256" + "internalType": "uint32", + "name": "_callbackGasLimit", + "type": "uint32" } ], "stateMutability": "nonpayable", @@ -2718,9 +2705,9 @@ "inputs": [ { "indexed": false, - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" } ], "name": "FulfilledRandomness", @@ -2731,9 +2718,9 @@ "inputs": [ { "indexed": false, - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" } ], "name": "RequestedRandomness", @@ -2744,9 +2731,9 @@ "name": "getRandomNumber", "outputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -2767,18 +2754,18 @@ }, { "inputs": [ - { - "internalType": "bytes32", - "name": "requestId", - "type": "bytes32" - }, { "internalType": "uint256", - "name": "randomness", + "name": "_requestId", "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "_randomWords", + "type": "uint256[]" } ], - "name": "rawFulfillRandomness", + "name": "rawFulfillRandomWords", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -2793,34 +2780,50 @@ ] }, "RandomSVG": { - "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c", "abi": [ { "inputs": [ { "internalType": "address", - "name": "_VRFCoordinator", + "name": "_vrfCoordinatorV2", "type": "address" }, { - "internalType": "address", - "name": "_LinkToken", - "type": "address" + "internalType": "uint64", + "name": "_vrfSubscriptionId", + "type": "uint64" }, { "internalType": "bytes32", - "name": "_keyhash", + "name": "_vrfGasLane", "type": "bytes32" }, { - "internalType": "uint256", - "name": "_fee", - "type": "uint256" + "internalType": "uint32", + "name": "_vrfCallbackGasLimit", + "type": "uint32" } ], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { + "internalType": "address", + "name": "have", + "type": "address" + }, + { + "internalType": "address", + "name": "want", + "type": "address" + } + ], + "name": "OnlyCoordinatorCanFulfill", + "type": "error" + }, { "anonymous": false, "inputs": [ @@ -2958,9 +2961,9 @@ "inputs": [ { "indexed": true, - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" }, { "indexed": true, @@ -3031,13 +3034,7 @@ { "inputs": [], "name": "create", - "outputs": [ - { - "internalType": "bytes32", - "name": "requestId", - "type": "bytes32" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -3266,17 +3263,17 @@ { "inputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "requestId", - "type": "bytes32" + "type": "uint256" }, { - "internalType": "uint256", - "name": "randomness", - "type": "uint256" + "internalType": "uint256[]", + "name": "randomWords", + "type": "uint256[]" } ], - "name": "rawFulfillRandomness", + "name": "rawFulfillRandomWords", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -3291,9 +3288,9 @@ { "inputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "", - "type": "bytes32" + "type": "uint256" } ], "name": "requestIdToSender", @@ -3310,9 +3307,9 @@ { "inputs": [ { - "internalType": "bytes32", + "internalType": "uint256", "name": "", - "type": "bytes32" + "type": "uint256" } ], "name": "requestIdToTokenId", @@ -3555,107 +3552,798 @@ } ] }, - "VRFCoordinatorMock": { - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "VRFCoordinatorV2Mock": { + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", "abi": [ { "inputs": [ { - "internalType": "address", - "name": "linkAddress", - "type": "address" + "internalType": "uint96", + "name": "_baseFee", + "type": "uint96" + }, + { + "internalType": "uint96", + "name": "_gasPriceLink", + "type": "uint96" } ], "stateMutability": "nonpayable", "type": "constructor" }, { - "anonymous": false, + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidConsumer", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidRandomWords", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSubscription", + "type": "error" + }, + { "inputs": [ { - "indexed": true, "internalType": "address", - "name": "sender", + "name": "owner", "type": "address" - }, + } + ], + "name": "MustBeSubOwner", + "type": "error" + }, + { + "inputs": [], + "name": "TooManyConsumers", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ { "indexed": true, - "internalType": "bytes32", - "name": "keyHash", - "type": "bytes32" + "internalType": "uint64", + "name": "subId", + "type": "uint64" }, { - "indexed": true, - "internalType": "uint256", - "name": "seed", - "type": "uint256" + "indexed": false, + "internalType": "address", + "name": "consumer", + "type": "address" } ], - "name": "RandomnessRequest", + "name": "ConsumerAdded", "type": "event" }, { - "inputs": [], - "name": "LINK", - "outputs": [ + "anonymous": false, + "inputs": [ { - "internalType": "contract LinkTokenInterface", - "name": "", + "indexed": true, + "internalType": "uint64", + "name": "subId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "consumer", "type": "address" } ], - "stateMutability": "view", - "type": "function" + "name": "ConsumerRemoved", + "type": "event" }, { + "anonymous": false, "inputs": [ { - "internalType": "bytes32", + "indexed": true, + "internalType": "uint256", "name": "requestId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "outputSeed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint96", + "name": "payment", + "type": "uint96" + }, + { + "indexed": false, + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "name": "RandomWordsFulfilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "keyHash", "type": "bytes32" }, { + "indexed": false, "internalType": "uint256", - "name": "randomness", + "name": "requestId", "type": "uint256" }, { + "indexed": false, + "internalType": "uint256", + "name": "preSeed", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "subId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "minimumRequestConfirmations", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "callbackGasLimit", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "numWords", + "type": "uint32" + }, + { + "indexed": true, "internalType": "address", - "name": "consumerContract", + "name": "sender", "type": "address" } ], - "name": "callBackWithRandomness", + "name": "RandomWordsRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "subId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "SubscriptionCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "subId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "SubscriptionCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "subId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldBalance", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "SubscriptionFunded", + "type": "event" + }, + { + "inputs": [], + "name": "BASE_FEE", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "GAS_PRICE_LINK", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_CONSUMERS", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + } + ], + "name": "acceptSubscriptionOwnerTransfer", + "outputs": [], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_consumer", + "type": "address" + } + ], + "name": "addConsumer", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, { "internalType": "address", - "name": "sender", + "name": "_to", "type": "address" + } + ], + "name": "cancelSubscription", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_consumer", + "type": "address" + } + ], + "name": "consumerIsAdded", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "createSubscription", + "outputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_requestId", + "type": "uint256" }, + { + "internalType": "address", + "name": "_consumer", + "type": "address" + } + ], + "name": "fulfillRandomWords", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", - "name": "fee", + "name": "_requestId", "type": "uint256" }, { - "internalType": "bytes", - "name": "_data", - "type": "bytes" + "internalType": "address", + "name": "_consumer", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "_words", + "type": "uint256[]" } ], - "name": "onTokenTransfer", + "name": "fulfillRandomWordsWithOverride", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, + { + "internalType": "uint96", + "name": "_amount", + "type": "uint96" + } + ], + "name": "fundSubscription", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getConfig", + "outputs": [ + { + "internalType": "uint16", + "name": "minimumRequestConfirmations", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "maxGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "stalenessSeconds", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "gasAfterPaymentCalculation", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFallbackWeiPerUnitLink", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getFeeConfig", + "outputs": [ + { + "internalType": "uint32", + "name": "fulfillmentFlatFeeLinkPPMTier1", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fulfillmentFlatFeeLinkPPMTier2", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fulfillmentFlatFeeLinkPPMTier3", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fulfillmentFlatFeeLinkPPMTier4", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "fulfillmentFlatFeeLinkPPMTier5", + "type": "uint32" + }, + { + "internalType": "uint24", + "name": "reqsForTier2", + "type": "uint24" + }, + { + "internalType": "uint24", + "name": "reqsForTier3", + "type": "uint24" + }, + { + "internalType": "uint24", + "name": "reqsForTier4", + "type": "uint24" + }, + { + "internalType": "uint24", + "name": "reqsForTier5", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getRequestConfig", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "", + "type": "uint32" + }, + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + } + ], + "name": "getSubscription", + "outputs": [ + { + "internalType": "uint96", + "name": "balance", + "type": "uint96" + }, + { + "internalType": "uint64", + "name": "reqCount", + "type": "uint64" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address[]", + "name": "consumers", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "subId", + "type": "uint64" + } + ], + "name": "pendingRequestExists", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_consumer", + "type": "address" + } + ], + "name": "removeConsumer", "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_keyHash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, + { + "internalType": "uint16", + "name": "_minimumRequestConfirmations", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "_callbackGasLimit", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "_numWords", + "type": "uint32" + } + ], + "name": "requestRandomWords", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_subId", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_newOwner", + "type": "address" + } + ], + "name": "requestSubscriptionOwnerTransfer", + "outputs": [], + "stateMutability": "pure", + "type": "function" + } + ] + }, + "VRFV2WrapperConsumerBaseMock": { + "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "keyHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "requestId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "preSeed", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "subId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "minimumRequestConfirmations", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "callbackGasLimit", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "numWords", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RandomWordsRequested", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "requestId", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "randomness", + "type": "uint256[]" + }, + { + "internalType": "address", + "name": "consumerContract", + "type": "address" + } + ], + "name": "callBackWithRandomness", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_callbackGasLimit", + "type": "uint32" + }, + { + "internalType": "uint16", + "name": "_minimumRequestConfirmations", + "type": "uint16" + }, + { + "internalType": "uint32", + "name": "_numWords", + "type": "uint32" + } + ], + "name": "requestRandomness", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "pure", + "type": "function" } ] } } } ] -} \ No newline at end of file +} diff --git a/packages/hardhat/contracts/RandomNumberConsumer.sol b/packages/hardhat/contracts/RandomNumberConsumer.sol index 64f24db..ba394f4 100644 --- a/packages/hardhat/contracts/RandomNumberConsumer.sol +++ b/packages/hardhat/contracts/RandomNumberConsumer.sol @@ -1,52 +1,55 @@ -pragma solidity 0.6.6; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.4; -import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol"; +import "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol"; -contract RandomNumberConsumer is VRFConsumerBase { - bytes32 internal keyHash; - uint256 internal fee; +contract RandomNumberConsumer is VRFV2WrapperConsumerBase { uint256 public randomResult; - event RequestedRandomness(bytes32 requestId); - event FulfilledRandomness(bytes32 requestId); + + uint32 internal callbackGasLimit; + // The default is 3, but you can set this higher. + uint16 internal requestConfirmations = 3; + // For this example, retrieve 2 random values in one request. + // Cannot exceed VRFV2Wrapper.getConfig().maxNumWords. + uint32 internal numWords = 1; + + event RequestedRandomness(uint256 requestId); + event FulfilledRandomness(uint256 requestId); /** * Constructor inherits VRFConsumerBase * * Network: Goerli - * Chainlink VRF Coordinator address: 0x2bce784e69d2Ff36c71edcB9F88358dB0DfB55b4 + * Chainlink VRF Wrapper address: 0x708701a1DfF4f478de54383E49a627eD4852C816 * LINK token address: 0x326C977E6efc84E512bB9C30f76E30c160eD06FB - * Key Hash: 0x0476f9a745b61ea5c0ab224d3a6e4c99f0b02fce4da01143a4f70aa80ae76e8a */ constructor( - address _vrfCoordinator, + address _wrapperAddress, address _link, - bytes32 _keyHash, - uint256 _fee + uint32 _callbackGasLimit ) - public - VRFConsumerBase( - _vrfCoordinator, // VRF Coordinator - _link // LINK Token + VRFV2WrapperConsumerBase( + _link, // LINK Token + _wrapperAddress // VRF Wrapper ) { - keyHash = _keyHash; - fee = _fee; + callbackGasLimit = _callbackGasLimit; } /** * Requests randomness */ - function getRandomNumber() public returns (bytes32 requestId) { - requestId = requestRandomness(keyHash, fee); + function getRandomNumber() public returns (uint256 requestId) { + requestId = requestRandomness(callbackGasLimit, requestConfirmations, numWords); emit RequestedRandomness(requestId); } /** * Callback function used by VRF Coordinator */ - function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override { - randomResult = randomness; - emit FulfilledRandomness(requestId); + function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override { + randomResult = _randomWords[0]; + emit FulfilledRandomness(_requestId); } /** diff --git a/packages/hardhat/contracts/RandomSVG.sol b/packages/hardhat/contracts/RandomSVG.sol index 0933753..0855044 100644 --- a/packages/hardhat/contracts/RandomSVG.sol +++ b/packages/hardhat/contracts/RandomSVG.sol @@ -1,37 +1,51 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.3; +pragma solidity 0.8.4; import "../vendor/openzeppelin/contracts/access/Ownable.sol"; import "../vendor/openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; import "base64-sol/base64.sol"; -import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; -contract RandomSVG is ERC721URIStorage, VRFConsumerBase, Ownable { - uint256 public tokenCounter; +contract RandomSVG is ERC721URIStorage, VRFConsumerBaseV2, Ownable { + // MUTABLE STORAGE - event CreatedRandomSVG(uint256 indexed tokenId, string tokenURI); - event CreatedUnfinishedRandomSVG(uint256 indexed tokenId, uint256 randomNumber); - event requestedRandomSVG(bytes32 indexed requestId, uint256 indexed tokenId); - mapping(bytes32 => address) public requestIdToSender; - mapping(uint256 => uint256) public tokenIdToRandomNumber; - mapping(bytes32 => uint256) public requestIdToTokenId; - bytes32 internal keyHash; - uint256 internal fee; + uint256 public tokenCounter; uint256 public maxNumberOfPaths; uint256 public maxNumberOfPathCommands; uint256 public size; string[] public pathCommands; string[] public colors; + mapping(uint256 => address) public requestIdToSender; + mapping(uint256 => uint256) public tokenIdToRandomNumber; + mapping(uint256 => uint256) public requestIdToTokenId; + + // VRF CONSTANTS & IMMUTABLE + + uint16 private constant vrfRequestConfirmations = 3; + uint32 private constant vrfNumWords = 1; + VRFCoordinatorV2Interface private immutable vrfCoordinatorV2; + uint64 private immutable vrfSubscriptionId; + bytes32 private immutable vrfGasLane; + uint32 private immutable vrfCallbackGasLimit; + + // EVENTS + + event CreatedRandomSVG(uint256 indexed tokenId, string tokenURI); + event CreatedUnfinishedRandomSVG(uint256 indexed tokenId, uint256 randomNumber); + event requestedRandomSVG(uint256 indexed requestId, uint256 indexed tokenId); constructor( - address _VRFCoordinator, - address _LinkToken, - bytes32 _keyhash, - uint256 _fee - ) VRFConsumerBase(_VRFCoordinator, _LinkToken) ERC721("RandomSVG", "rsNFT") { - tokenCounter = 0; - keyHash = _keyhash; - fee = _fee; + address _vrfCoordinatorV2, + uint64 _vrfSubscriptionId, + bytes32 _vrfGasLane, + uint32 _vrfCallbackGasLimit + ) VRFConsumerBaseV2(_vrfCoordinatorV2) ERC721("RandomSVG", "rsNFT") { + vrfCoordinatorV2 = VRFCoordinatorV2Interface(_vrfCoordinatorV2); + vrfSubscriptionId = _vrfSubscriptionId; + vrfGasLane = _vrfGasLane; + vrfCallbackGasLimit = _vrfCallbackGasLimit; + maxNumberOfPaths = 10; maxNumberOfPathCommands = 5; size = 500; @@ -39,12 +53,16 @@ contract RandomSVG is ERC721URIStorage, VRFConsumerBase, Ownable { colors = ["red", "blue", "green", "yellow", "black", "white"]; } - function withdraw() public payable onlyOwner { - payable(owner()).transfer(address(this).balance); - } + // ACTIONS - function create() public returns (bytes32 requestId) { - requestId = requestRandomness(keyHash, fee); + function create() public { + uint256 requestId = vrfCoordinatorV2.requestRandomWords( + vrfGasLane, + vrfSubscriptionId, + vrfRequestConfirmations, + vrfCallbackGasLimit, + vrfNumWords + ); requestIdToSender[requestId] = msg.sender; uint256 tokenId = tokenCounter; requestIdToTokenId[requestId] = tokenId; @@ -63,14 +81,44 @@ contract RandomSVG is ERC721URIStorage, VRFConsumerBase, Ownable { emit CreatedRandomSVG(tokenId, svg); } - function fulfillRandomness(bytes32 requestId, uint256 randomNumber) internal override { + function withdraw() public payable onlyOwner { + payable(owner()).transfer(address(this).balance); + } + + // VRF + + function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { address nftOwner = requestIdToSender[requestId]; uint256 tokenId = requestIdToTokenId[requestId]; _safeMint(nftOwner, tokenId); - tokenIdToRandomNumber[tokenId] = randomNumber; - emit CreatedUnfinishedRandomSVG(tokenId, randomNumber); + tokenIdToRandomNumber[tokenId] = randomWords[0]; + emit CreatedUnfinishedRandomSVG(tokenId, randomWords[0]); + } + + // GETTERS + + function formatTokenURI(string memory imageURI) public pure returns (string memory) { + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode( + bytes( + abi.encodePacked( + '{"name":"', + "SVG NFT", // You can add whatever name here + '", "description":"An NFT based on SVG!", "attributes":"", "image":"', + imageURI, + '"}' + ) + ) + ) + ) + ); } + // HELPERS + function generateSVG(uint256 _randomness) public view returns (string memory finalSvg) { // We will only use the path element, with stroke and d elements uint256 numberOfPaths = (_randomness % maxNumberOfPaths) + 1; @@ -141,24 +189,4 @@ contract RandomSVG is ERC721URIStorage, VRFConsumerBase, Ownable { string memory svgBase64Encoded = Base64.encode(bytes(string(abi.encodePacked(svg)))); return string(abi.encodePacked(baseURL, svgBase64Encoded)); } - - function formatTokenURI(string memory imageURI) public pure returns (string memory) { - return - string( - abi.encodePacked( - "data:application/json;base64,", - Base64.encode( - bytes( - abi.encodePacked( - '{"name":"', - "SVG NFT", // You can add whatever name here - '", "description":"An NFT based on SVG!", "attributes":"", "image":"', - imageURI, - '"}' - ) - ) - ) - ) - ); - } } diff --git a/packages/hardhat/contracts/mocks/VRFCoordinatorMock.sol b/packages/hardhat/contracts/mocks/VRFCoordinatorMock.sol deleted file mode 100644 index 127bdf0..0000000 --- a/packages/hardhat/contracts/mocks/VRFCoordinatorMock.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.6.6; - -import "@chainlink/contracts/src/v0.6/tests/VRFCoordinatorMock.sol"; diff --git a/packages/hardhat/contracts/mocks/VRFCoordinatorV2Mock.sol b/packages/hardhat/contracts/mocks/VRFCoordinatorV2Mock.sol new file mode 100644 index 0000000..7f04cc8 --- /dev/null +++ b/packages/hardhat/contracts/mocks/VRFCoordinatorV2Mock.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; diff --git a/packages/hardhat/contracts/mocks/VRFV2Wrapper.sol b/packages/hardhat/contracts/mocks/VRFV2Wrapper.sol new file mode 100644 index 0000000..a3c1eaf --- /dev/null +++ b/packages/hardhat/contracts/mocks/VRFV2Wrapper.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.6; + +import "@chainlink/contracts/src/v0.8/VRFV2Wrapper.sol"; diff --git a/packages/hardhat/deploy/01_Deploy_Mocks.ts b/packages/hardhat/deploy/01_Deploy_Mocks.ts index 5b7704a..986c10d 100644 --- a/packages/hardhat/deploy/01_Deploy_Mocks.ts +++ b/packages/hardhat/deploy/01_Deploy_Mocks.ts @@ -1,14 +1,20 @@ +import { ethers } from 'hardhat' +import { BigNumber } from 'ethers' +import { formatBytes32String } from 'ethers/lib/utils' import { HardhatRuntimeEnvironment } from 'hardhat/types' import { DeployFunction } from 'hardhat-deploy/types' +import { VRFV2Wrapper } from 'types/typechain' + +const DECIMALS = '18' +const INITIAL_PRICE = BigNumber.from('3000000000000000') +const BASE_FEE = '100000000000000000' +const GAS_PRICE_LINK = '1000000000' // 0.000000001 LINK per gas const func: DeployFunction = async function ({ deployments, getNamedAccounts, getChainId, }: HardhatRuntimeEnvironment) { - const DECIMALS = '8' - const INITIAL_PRICE = '20000000000' - const { deploy } = deployments const { deployer } = await getNamedAccounts() const chainId = await getChainId() @@ -18,18 +24,29 @@ const func: DeployFunction = async function ({ from: deployer, log: true, }) - await deploy('EthUsdAggregator', { + + const aggregator = await deploy('EthUsdAggregator', { contract: 'MockV3Aggregator', from: deployer, log: true, args: [DECIMALS, INITIAL_PRICE], }) + const linkToken = await deploy('LinkToken', { from: deployer, log: true }) - await deploy('VRFCoordinatorMock', { + + const coordinator = await deploy('VRFCoordinatorV2Mock', { from: deployer, log: true, - args: [linkToken.address], + args: [BASE_FEE, GAS_PRICE_LINK], }) + + const vrfV2Wrapper = await deploy('VRFV2Wrapper', { + from: deployer, + log: true, + args: [linkToken.address, aggregator.address, coordinator.address], + }) + await configureWrapper(vrfV2Wrapper.address) + await deploy('MockOracle', { from: deployer, log: true, @@ -38,6 +55,26 @@ const func: DeployFunction = async function ({ } } +const WRAPPER_GAS_OVERHEAD = BigNumber.from(60000) +const COORDINATOR_GAS_OVERHEAD = BigNumber.from(52000) +const WRAPPER_PREMIUM_PERCENTAGE = 10 +const MAX_NUM_WORDS = 5 + +async function configureWrapper(wrapperAddress: string) { + const wrapper = (await ethers.getContractAt( + 'VRFV2Wrapper', + wrapperAddress + )) as VRFV2Wrapper + + await wrapper.setConfig( + WRAPPER_GAS_OVERHEAD, + COORDINATOR_GAS_OVERHEAD, + WRAPPER_PREMIUM_PERCENTAGE, + formatBytes32String('keyHash'), + MAX_NUM_WORDS + ) +} + func.tags = ['all', 'mocks', 'main'] export default func diff --git a/packages/hardhat/deploy/04_Deploy_RandomNumberConsumer.ts b/packages/hardhat/deploy/04_Deploy_RandomNumberConsumer.ts index a2778c4..621b7f3 100644 --- a/packages/hardhat/deploy/04_Deploy_RandomNumberConsumer.ts +++ b/packages/hardhat/deploy/04_Deploy_RandomNumberConsumer.ts @@ -11,22 +11,26 @@ const func: DeployFunction = async function ({ const { deployer } = await getNamedAccounts() const chainId = await getChainId() let linkTokenAddress: string - let vrfCoordinatorAddress: string + let wrapperAddress: string if (chainId === '31337') { const LinkToken = await get('LinkToken') linkTokenAddress = LinkToken.address - const VRFCoordinatorMock = await get('VRFCoordinatorMock') - vrfCoordinatorAddress = VRFCoordinatorMock.address + const VRFV2Wrapper = await get('VRFV2Wrapper') + wrapperAddress = VRFV2Wrapper.address } else { linkTokenAddress = networkConfig[chainId].linkToken as string - vrfCoordinatorAddress = networkConfig[chainId].vrfCoordinator as string + wrapperAddress = networkConfig[chainId].wrapperAddress as string } - const { keyHash, fee } = networkConfig[chainId] + const { vrfCallbackGasLimit } = networkConfig[chainId] await deploy('RandomNumberConsumer', { from: deployer, - args: [vrfCoordinatorAddress, linkTokenAddress, keyHash, fee], + args: [ + wrapperAddress, + linkTokenAddress, + vrfCallbackGasLimit?.randomNumberConsumer, + ], log: true, }) } diff --git a/packages/hardhat/deploy/05_Deploy_RandomSVG.ts b/packages/hardhat/deploy/05_Deploy_RandomSVG.ts index 9a7be9f..6e8ccc4 100644 --- a/packages/hardhat/deploy/05_Deploy_RandomSVG.ts +++ b/packages/hardhat/deploy/05_Deploy_RandomSVG.ts @@ -1,34 +1,109 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { DeployFunction } from 'hardhat-deploy/types' +import { ethers } from 'hardhat' import { networkConfig } from '../helper-hardhat-config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { DeployFunction, DeploymentsExtension } from 'hardhat-deploy/types' +import { VRFCoordinatorV2Mock } from 'types/typechain' const func: DeployFunction = async function ({ deployments, getNamedAccounts, getChainId, }: HardhatRuntimeEnvironment) { - const { deploy, get } = deployments const { deployer } = await getNamedAccounts() const chainId = await getChainId() - let linkTokenAddress: string - let vrfCoordinatorAddress: string if (chainId === '31337') { - const LinkToken = await get('LinkToken') - linkTokenAddress = LinkToken.address - const VRFCoordinatorMock = await get('VRFCoordinatorMock') - vrfCoordinatorAddress = VRFCoordinatorMock.address + await deployToLocalNetwork(deployments, deployer, chainId) } else { - linkTokenAddress = networkConfig[chainId].linkToken as string - vrfCoordinatorAddress = networkConfig[chainId].vrfCoordinator as string + await deployToPublicNetwork(deployments, deployer, chainId) } - const { keyHash, fee } = networkConfig[chainId] +} + +async function deployToLocalNetwork( + deployments: DeploymentsExtension, + deployer: string, + chainId: string +) { + const { deploy, get } = deployments + const { vrfGasLane, vrfCallbackGasLimit } = networkConfig[chainId] + + const VRFCoordinatorMockV2 = await get('VRFCoordinatorV2Mock') + const vrfCoordinatorV2Mock = (await ethers.getContractAt( + 'VRFCoordinatorV2Mock', + VRFCoordinatorMockV2.address + )) as VRFCoordinatorV2Mock + + const vrfCoordinatorV2 = vrfCoordinatorV2Mock.address + const vrfSubscriptionId = await createMockSubscription( + vrfCoordinatorV2Mock, + chainId + ) await deploy('RandomSVG', { from: deployer, - args: [vrfCoordinatorAddress, linkTokenAddress, keyHash, fee], + args: [ + vrfCoordinatorV2, + vrfSubscriptionId, + vrfGasLane, + vrfCallbackGasLimit?.randomSVG, + ], log: true, }) + + const RandomSVG = await get('RandomSVG') + + const consumerIsAdded = await vrfCoordinatorV2Mock.consumerIsAdded( + vrfSubscriptionId, + RandomSVG.address + ) + if (!consumerIsAdded) { + await vrfCoordinatorV2Mock?.addConsumer( + vrfSubscriptionId, + RandomSVG.address + ) + } +} + +async function deployToPublicNetwork( + deployments: DeploymentsExtension, + deployer: string, + chainId: string +) { + const { deploy } = deployments + const { + vrfCoordinatorV2, + vrfSubscriptionId, + vrfGasLane, + vrfCallbackGasLimit, + } = networkConfig[chainId] + + await deploy('RandomSVG', { + from: deployer, + args: [ + vrfCoordinatorV2, + vrfSubscriptionId, + vrfGasLane, + vrfCallbackGasLimit?.randomSVG, + ], + log: true, + }) +} + +async function createMockSubscription( + vrfCoordinatorV2Mock: VRFCoordinatorV2Mock, + chainId: string +): Promise { + const { fundAmount } = networkConfig[chainId] + + const transaction = await vrfCoordinatorV2Mock.createSubscription() + const transactionReceipt = await transaction.wait() + const vrfSubscriptionId = ethers.BigNumber.from( + transactionReceipt!.events![0].topics[1] + ).toString() + + await vrfCoordinatorV2Mock.fundSubscription(vrfSubscriptionId, fundAmount) + + return vrfSubscriptionId } func.tags = ['all', 'vrf', 'nft', 'main'] diff --git a/packages/hardhat/deploy/07_Setup_Contracts.ts b/packages/hardhat/deploy/07_Setup_Contracts.ts index c694a04..998fa8c 100644 --- a/packages/hardhat/deploy/07_Setup_Contracts.ts +++ b/packages/hardhat/deploy/07_Setup_Contracts.ts @@ -20,7 +20,7 @@ const func: DeployFunction = async function ({ linkTokenAddress = networkConfig[chainId].linkToken as string } - // Try Auto-fund VRFConsumer contract + // Try Auto-fund RandomNumberConsumer contract const RandomNumberConsumer = await deployments.get('RandomNumberConsumer') const randomNumberConsumer = await ethers.getContractAt( 'RandomNumberConsumer', @@ -36,15 +36,11 @@ const func: DeployFunction = async function ({ }) } - // Try Auto-fund RandomSVG contract - const RandomSVG = await deployments.get('RandomSVG') - const randomSVG = await ethers.getContractAt('RandomSVG', RandomSVG.address) - if (await autoFundCheck(randomSVG.address, chainId, linkTokenAddress)) { - await run('fund-link', { - contract: randomSVG.address, - linkaddress: linkTokenAddress, - fundamount: networkConfig[chainId].fundAmount, - }) + // Fund RandomSVG instructions + if (chainId !== '31337') { + console.log( + `Please add RandomSVG address in your prefunded Chainlink VRF sucscription at https://vrf.chain.link` + ) } // Try Auto-fund APIConsumer contract with LINK diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index d52bf18..cbadd1b 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -37,8 +37,7 @@ const ETHERSCAN_API_KEY = const config: HardhatUserConfig = { defaultNetwork: 'localhost', networks: { - hardhat: { - }, + hardhat: {}, localhost: { url: 'http://localhost:8545', }, @@ -70,6 +69,12 @@ const config: HardhatUserConfig = { }, solidity: { compilers: [ + { + version: '0.8.6', + }, + { + version: '0.8.4', + }, { version: '0.8.3', }, diff --git a/packages/hardhat/helper-hardhat-config.ts b/packages/hardhat/helper-hardhat-config.ts index 78e19ae..eed62f5 100644 --- a/packages/hardhat/helper-hardhat-config.ts +++ b/packages/hardhat/helper-hardhat-config.ts @@ -5,7 +5,14 @@ export const networkConfig: Record< linkToken?: string feedRegistry?: string ethUsdPriceFeed?: string - vrfCoordinator?: string + wrapperAddress?: string + vrfCoordinatorV2?: string + vrfSubscriptionId?: string + vrfGasLane?: string + vrfCallbackGasLimit?: { + randomNumberConsumer: string + randomSVG: string + } keyHash: string oracle?: string jobId: string @@ -18,20 +25,34 @@ export const networkConfig: Record< keyHash: '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4', jobId: '29fa9aa13bf1468788b7cc4a500a45b8', + vrfGasLane: + '0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15', + vrfCallbackGasLimit: { + randomNumberConsumer: '500000', + randomSVG: '500000', + }, fee: '100000000000000000', - fundAmount: '1000000000000000000', + fundAmount: '50000000000000000000', }, '5': { name: 'goerli', linkToken: '0x326C977E6efc84E512bB9C30f76E30c160eD06FB', ethUsdPriceFeed: '0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e', - vrfCoordinator: '0x2bce784e69d2Ff36c71edcB9F88358dB0DfB55b4', + wrapperAddress: '0x708701a1DfF4f478de54383E49a627eD4852C816', + vrfCoordinatorV2: '0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D', + vrfSubscriptionId: '0', + vrfGasLane: + '0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15', + vrfCallbackGasLimit: { + randomNumberConsumer: '150000', + randomSVG: '200000', + }, keyHash: '0x0476f9a745b61ea5c0ab224d3a6e4c99f0b02fce4da01143a4f70aa80ae76e8a', oracle: '0xCC79157eb46F5624204f47AB42b3906cAA40eaB7', jobId: 'ca98366cc7314957b8c012c72f05aeeb', fee: '100000000000000000', - fundAmount: '1000000000000000000', + fundAmount: '5000000000000000000', }, } diff --git a/packages/hardhat/test/unit/RandomSVG.test.ts b/packages/hardhat/test/unit/RandomSVG.test.ts index 3186eee..64d4d13 100644 --- a/packages/hardhat/test/unit/RandomSVG.test.ts +++ b/packages/hardhat/test/unit/RandomSVG.test.ts @@ -1,69 +1,52 @@ import fs from 'fs' -import { ethers, deployments, network, getChainId, run } from 'hardhat' +import { ethers, deployments, network } from 'hardhat' import { expect } from 'chai' import skip from 'mocha-skip-if' -import { developmentChains, networkConfig } from '../../helper-hardhat-config' -import { autoFundCheck } from '../../utils' -import { RandomSVG, VRFCoordinatorMock, LinkToken } from 'types/typechain' +import { developmentChains } from '../../helper-hardhat-config' +import { RandomSVG, VRFCoordinatorV2Mock } from 'types/typechain' skip .if(!developmentChains.includes(network.name)) .describe('RandomSVG Unit Tests', () => { - let rsNFT: RandomSVG, - linkToken: LinkToken, - vrfCoordinator: VRFCoordinatorMock + let randomSvg: RandomSVG + let vrfCoordinatorV2: VRFCoordinatorV2Mock beforeEach(async () => { - const chainId = await getChainId() await deployments.fixture(['mocks', 'vrf', 'nft']) - const LinkToken = await deployments.get('LinkToken') - linkToken = (await ethers.getContractAt( - 'LinkToken', - LinkToken.address - )) as LinkToken - const linkTokenAddress = linkToken.address + const VRFCoordinatorMockV2 = await deployments.get('VRFCoordinatorV2Mock') + vrfCoordinatorV2 = (await ethers.getContractAt( + 'VRFCoordinatorV2Mock', + VRFCoordinatorMockV2.address + )) as VRFCoordinatorV2Mock const RandomSVG = await deployments.get('RandomSVG') - rsNFT = (await ethers.getContractAt( + randomSvg = (await ethers.getContractAt( 'RandomSVG', RandomSVG.address )) as RandomSVG - - const VRFCoordinatorMock = await deployments.get('VRFCoordinatorMock') - vrfCoordinator = (await ethers.getContractAt( - 'VRFCoordinatorMock', - VRFCoordinatorMock.address - )) as VRFCoordinatorMock - - if (await autoFundCheck(rsNFT.address, chainId, linkTokenAddress)) { - await run('fund-link', { - contract: rsNFT.address, - linkaddress: linkTokenAddress, - fundamount: networkConfig[chainId].fundAmount, - }) - } }) it('should return the correct URI', async () => { - const transactionCreate = await rsNFT.create() + const transactionCreate = await randomSvg.create() const receipt = await transactionCreate.wait() const [, requestId, tokenId] = - (receipt.events && receipt.events[3].topics) || [] + (receipt.events && receipt.events[1].topics) || [] const fakeRandomNumber = 77777 - const transactionResponse = await vrfCoordinator.callBackWithRandomness( - requestId, - fakeRandomNumber, - rsNFT.address - ) - await transactionResponse.wait() + const transactionResponse = + await vrfCoordinatorV2.fulfillRandomWordsWithOverride( + requestId, + randomSvg.address, + [fakeRandomNumber] + ) - const transactionMint = await rsNFT.finishMint(tokenId) + await transactionResponse.wait() + const transactionMint = await randomSvg.finishMint(tokenId) await transactionMint.wait() const expectedURI = fs.readFileSync('./test/data/randomSVG.txt', 'utf8') - const uri = await rsNFT.tokenURI(0) + const uri = await randomSvg.tokenURI(0) expect(uri).to.equal(expectedURI) }) })