├── README.md └── hardhat-tutorial ├── .gitignore ├── README.md ├── constants └── index.js ├── contracts ├── Greeter.sol └── RandomWinnerGame.sol ├── hardhat.config.js ├── package-lock.json ├── package.json ├── scripts ├── deploy.js └── sample-script.js └── test └── sample-test.js /README.md: -------------------------------------------------------------------------------- 1 | # Chainlink VRFs 2 | 3 | ## Introduction 4 | 5 | When dealing with computers, randomness is an important but difficult issue to handle due to a computer's deterministic nature. This is true even more so when speaking of blockchain because not only is the computer deterministic, but it is also transparent. As a result, trusted random numbers cannot be generated natively in Solidity because randomness will be calculated on-chain which is public info to all the miners and the users. 6 | 7 | So we can use some web2 technologies to generate the randomness and then use them on-chain. 8 | 9 | ## What is an oracle? 10 | 11 | - An oracle sends data from the outside world to a blockchain's smart contract and vice-verca. 12 | - Smart contract can then use this data to make a decision and change its state. 13 | - They act as bridges between blockchains and the external world. 14 | - However it is important to note that the blockchain oracle is not itself the data source but its job is to query, verify and authenticate the outside data and then futher pass it to the smart contract. 15 | 16 | Today we will learn about one of oracles named Chainlink VRF's 17 | 18 | Lets goo 🚀 19 | 20 | ## Intro 21 | 22 | - Chainlink VRF's are oracles which used to generate random values. 23 | - These values are verified using cryptographic proofs. 24 | - These proofs prove that the results weren't tampered or manipulated by oracle operators, users, miners etc. 25 | - Proofs are published on-chain so that they can be verified. 26 | - After there verification is successful they are used by smart contracts which requested randomness. 27 | 28 | The official Chainlink Docs describe VRFs as: 29 | 30 | > Chainlink VRF (Verifiable Random Function) is a provably-fair and verifiable source of randomness designed for smart contracts. Smart contract developers can use Chainlink VRF as a tamper-proof random number generator (RNG) to build reliable smart contracts for any applications which rely on unpredictable outcomes. 31 | 32 | ## How does it work? 33 | 34 | - Chainlink has two contracts that we are mostly concerned about [VRFConsumerBase.sol](https://github.com/smartcontractkit/chainlink/blob/master/contracts/src/v0.8/VRFConsumerBase.sol) and VRFCoordinator 35 | - VRFConsumerBase is the contract that will be calling the VRF Coordinator which is finally reponsible for publishing the randomness 36 | - We will be inheriting VRFConsumerBase and will be using two functions from it: 37 | - requestRandomness, which makes the initial request for randomness. 38 | - fulfillRandomness, which is the function that receives and does something with verified randomness. 39 | 40 | ![](https://i.imgur.com/ssQTlkc.png) 41 | 42 | - If you look at the diagram you can understand the flow, `RandomGameWinner` contract will inherit the `VRFConsumerBase` contract and will call the `requestRandomness` function within the `VRFConsumerBase`. 43 | - On calling that function the request to randomness starts and the `VRFConsumerBase` further calls the `VRFCoordinator` contract which is reponsible for getting the randomness back from the external world. 44 | - After the `VRFCoordinator` has the randomness it calls the `fullFillRandomness` function within the `VRFConsumerBase` which further then selects the winner. 45 | - **Note the important part is that eventhough you called the `requestRandomness` function you get the randomness back in the `fullFillRandomness` function** 46 | 47 | ## Prerequisites 48 | 49 | - You have completed [Hardhat Verification](https://github.com/LearnWeb3DAO/hardhat-verification) module 50 | - You have completed the Layer 2 tutorials 51 | 52 | ## Requirements 53 | 54 | - We will build a lottery game today 55 | - Each game will have a max number of players and an entry fee 56 | - After max number of players have entered the game, one winner is chosen at random 57 | - The winner will get `maxplayers*entryfee` amount of ether for winning the game 58 | 59 | ## BUIDL IT 60 | 61 | - Initially start by creating a folder named `RandomWinnerGame` in your computer 62 | - To build the smart contract we would be using [Hardhat](https://hardhat.org/). Hardhat is an Ethereum development environment and framework designed for full stack development in Solidity. In simple words you can write your smart contract, deploy them, run tests, and debug your code. 63 | 64 | - To setup a Hardhat project, Open up a terminal and execute these commands inside the `RandomWinnerGame` folder 65 | 66 | ```bash 67 | mkdir hardhat-tutorial 68 | cd hardhat-tutorial 69 | npm init --yes 70 | npm install --save-dev hardhat 71 | ``` 72 | 73 | - In the same directory where you installed Hardhat run: 74 | 75 | ```bash 76 | npx hardhat 77 | ``` 78 | 79 | - Select `Create a Javascript project` 80 | - Press enter for the already specified `Hardhat Project root` 81 | - Press enter for the question on if you want to add a `.gitignore` 82 | - Press enter for `Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?` 83 | 84 | Now you have a hardhat project ready to go! 85 | 86 | If you are not on mac, please do this extra step and install these libraries as well :) 87 | 88 | ```bash 89 | npm install --save-dev @nomicfoundation/hardhat-toolbox 90 | ``` 91 | 92 | and press `enter` for all the questions. 93 | 94 | - In the same terminal now install `@openzeppelin/contracts` as we would be importing Openzeppelin's Contracts 95 | 96 | ```bash 97 | npm install @openzeppelin/contracts 98 | ``` 99 | 100 | - We will also be verifying our contracts, so lets install hardhat etherscan library 101 | 102 | ```bash 103 | npm install --save-dev @nomiclabs/hardhat-etherscan 104 | ``` 105 | 106 | - Lastly we will install the chainlink contracts, to use Chainlink VRF 107 | 108 | ```bash 109 | npm install --save @chainlink/contracts 110 | ``` 111 | 112 | - Now create a new file inside the `contracts` directory called `RandomWinnerGame.sol` and paste the following lines of code: 113 | 114 | ```solidity 115 | // SPDX-License-Identifier: MIT 116 | pragma solidity ^0.8.4; 117 | 118 | import "@openzeppelin/contracts/access/Ownable.sol"; 119 | import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; 120 | 121 | contract RandomWinnerGame is VRFConsumerBase, Ownable { 122 | 123 | //Chainlink variables 124 | // The amount of LINK to send with the request 125 | uint256 public fee; 126 | // ID of public key against which randomness is generated 127 | bytes32 public keyHash; 128 | 129 | // Address of the players 130 | address[] public players; 131 | //Max number of players in one game 132 | uint8 maxPlayers; 133 | // Variable to indicate if the game has started or not 134 | bool public gameStarted; 135 | // the fees for entering the game 136 | uint256 entryFee; 137 | // current game id 138 | uint256 public gameId; 139 | 140 | // emitted when the game starts 141 | event GameStarted(uint256 gameId, uint8 maxPlayers, uint256 entryFee); 142 | // emitted when someone joins a game 143 | event PlayerJoined(uint256 gameId, address player); 144 | // emitted when the game ends 145 | event GameEnded(uint256 gameId, address winner,bytes32 requestId); 146 | 147 | /** 148 | * constructor inherits a VRFConsumerBase and initiates the values for keyHash, fee and gameStarted 149 | * @param vrfCoordinator address of VRFCoordinator contract 150 | * @param linkToken address of LINK token contract 151 | * @param vrfFee the amount of LINK to send with the request 152 | * @param vrfKeyHash ID of public key against which randomness is generated 153 | */ 154 | constructor(address vrfCoordinator, address linkToken, 155 | bytes32 vrfKeyHash, uint256 vrfFee) 156 | VRFConsumerBase(vrfCoordinator, linkToken) { 157 | keyHash = vrfKeyHash; 158 | fee = vrfFee; 159 | gameStarted = false; 160 | } 161 | 162 | /** 163 | * startGame starts the game by setting appropriate values for all the variables 164 | */ 165 | function startGame(uint8 _maxPlayers, uint256 _entryFee) public onlyOwner { 166 | // Check if there is a game already running 167 | require(!gameStarted, "Game is currently running"); 168 | // empty the players array 169 | delete players; 170 | // set the max players for this game 171 | maxPlayers = _maxPlayers; 172 | // set the game started to true 173 | gameStarted = true; 174 | // setup the entryFee for the game 175 | entryFee = _entryFee; 176 | gameId += 1; 177 | emit GameStarted(gameId, maxPlayers, entryFee); 178 | } 179 | 180 | /** 181 | joinGame is called when a player wants to enter the game 182 | */ 183 | function joinGame() public payable { 184 | // Check if a game is already running 185 | require(gameStarted, "Game has not been started yet"); 186 | // Check if the value sent by the user matches the entryFee 187 | require(msg.value == entryFee, "Value sent is not equal to entryFee"); 188 | // Check if there is still some space left in the game to add another player 189 | require(players.length < maxPlayers, "Game is full"); 190 | // add the sender to the players list 191 | players.push(msg.sender); 192 | emit PlayerJoined(gameId, msg.sender); 193 | // If the list is full start the winner selection process 194 | if(players.length == maxPlayers) { 195 | getRandomWinner(); 196 | } 197 | } 198 | 199 | /** 200 | * fulfillRandomness is called by VRFCoordinator when it receives a valid VRF proof. 201 | * This function is overrided to act upon the random number generated by Chainlink VRF. 202 | * @param requestId this ID is unique for the request we sent to the VRF Coordinator 203 | * @param randomness this is a random unit256 generated and returned to us by the VRF Coordinator 204 | */ 205 | function fulfillRandomness(bytes32 requestId, uint256 randomness) internal virtual override { 206 | // We want out winnerIndex to be in the length from 0 to players.length-1 207 | // For this we mod it with the player.length value 208 | uint256 winnerIndex = randomness % players.length; 209 | // get the address of the winner from the players array 210 | address winner = players[winnerIndex]; 211 | // send the ether in the contract to the winner 212 | (bool sent,) = winner.call{value: address(this).balance}(""); 213 | require(sent, "Failed to send Ether"); 214 | // Emit that the game has ended 215 | emit GameEnded(gameId, winner,requestId); 216 | // set the gameStarted variable to false 217 | gameStarted = false; 218 | } 219 | 220 | /** 221 | * getRandomWinner is called to start the process of selecting a random winner 222 | */ 223 | function getRandomWinner() private returns (bytes32 requestId) { 224 | // LINK is an internal interface for Link token found within the VRFConsumerBase 225 | // Here we use the balanceOF method from that interface to make sure that our 226 | // contract has enough link so that we can request the VRFCoordinator for randomness 227 | require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK"); 228 | // Make a request to the VRF coordinator. 229 | // requestRandomness is a function within the VRFConsumerBase 230 | // it starts the process of randomness generation 231 | return requestRandomness(keyHash, fee); 232 | } 233 | 234 | // Function to receive Ether. msg.data must be empty 235 | receive() external payable {} 236 | 237 | // Fallback function is called when msg.data is not empty 238 | fallback() external payable {} 239 | } 240 | ``` 241 | 242 | - The constructor takes in the following params: 243 | - `vrfCoordinator` which is the address of the VRFCoordinator contract 244 | - `linkToken` is the address of the link token which is the token in which the chainlink takes its payment 245 | - `vrfFee` is the amount of link token that will be needed to send a randomness request 246 | - `vrfKeyHash` which is the ID of the public key against which randomness is generated. This value is responsible for generating an unique Id for our randomneses request called as `requestId` 247 | 248 | **(All these values are provided to us by Chainlink)** 249 | 250 | ```solidity 251 | /** 252 | * startGame starts the game by setting appropriate values for all the variables 253 | */ 254 | function startGame(uint8 _maxPlayers, uint256 _entryFee) public onlyOwner { 255 | // Check if there is a game already running 256 | require(!gameStarted, "Game is currently running"); 257 | // empty the players array 258 | delete players; 259 | // set the max players for this game 260 | maxPlayers = _maxPlayers; 261 | // set the game started to true 262 | gameStarted = true; 263 | // setup the entryFee for the game 264 | entryFee = _entryFee; 265 | gameId += 1; 266 | emit GameStarted(gameId, maxPlayers, entryFee); 267 | } 268 | ``` 269 | 270 | - This function is `onlyOwner` which means that it can only be called by the owner 271 | - This function is used to start the game, after this function is called players can enter the game until limit has been achieved. 272 | - It also emits the `GameStarted` event 273 | 274 | ```solidity 275 | /** 276 | joinGame is called when a player wants to enter the game 277 | */ 278 | function joinGame() public payable { 279 | // Check if a game is already running 280 | require(gameStarted, "Game has not been started yet"); 281 | // Check if the value sent by the user matches the entryFee 282 | require(msg.value == entryFee, "Value sent is not equal to entryFee"); 283 | // Check if there is still some space left in the game to add another player 284 | require(players.length < maxPlayers, "Game is full"); 285 | // add the sender to the players list 286 | players.push(msg.sender); 287 | emit PlayerJoined(gameId, msg.sender); 288 | // If the list is full start the winner selection process 289 | if(players.length == maxPlayers) { 290 | getRandomWinner(); 291 | } 292 | } 293 | ``` 294 | 295 | - This function will be called when a user wants to enter a game 296 | - If the `maxPlayers` limit is reached it will call the `getRandomWinner` function 297 | 298 | ```solidity 299 | /** 300 | * getRandomWinner is called to start the process of selecting a random winner 301 | */ 302 | function getRandomWinner() private returns (bytes32 requestId) { 303 | // LINK is an internal interface for Link token found within the VRFConsumerBase 304 | // Here we use the balanceOF method from that interface to make sure that our 305 | // contract has enough link so that we can request the VRFCoordinator for randomness 306 | require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK"); 307 | // Make a request to the VRF coordinator. 308 | // requestRandomness is a function within the VRFConsumerBase 309 | // it starts the process of randomness generation 310 | return requestRandomness(keyHash, fee); 311 | } 312 | ``` 313 | 314 | - This function first checks if our contract has Link token before we request for randomness because chainlink contracts request fee in the form of Link token 315 | - Then this function calls the `requestRandomness` which we inherited from `VRFConsumerBase` and begins the process for random number generation. 316 | 317 | ```solidity 318 | /** 319 | * fulfillRandomness is called by VRFCoordinator when it receives a valid VRF proof. 320 | * This function is overrided to act upon the random number generated by Chainlink VRF. 321 | * @param requestId this ID is unique for the request we sent to the VRF Coordinator 322 | * @param randomness this is a random unit256 generated and returned to us by the VRF Coordinator 323 | */ 324 | function fulfillRandomness(bytes32 requestId, uint256 randomness) internal virtual override { 325 | // We want out winnerIndex to be in the length from 0 to players.length-1 326 | // For this we mod it with the player.length value 327 | uint256 winnerIndex = randomness % players.length; 328 | // get the address of the winner from the players array 329 | address winner = players[winnerIndex]; 330 | // send the ether in the contract to the winner 331 | (bool sent,) = winner.call{value: address(this).balance}(""); 332 | require(sent, "Failed to send Ether"); 333 | // Emit that the game has ended 334 | emit GameEnded(gameId, winner,requestId); 335 | // set the gameStarted variable to false 336 | gameStarted = false; 337 | } 338 | ``` 339 | 340 | - This function was inherited from `VRFConsumerBase`. It is called by `VRFCoordinator` contract after it recieves the randomness from the external world. 341 | - After recieving the randomness which can be any number in the range of uint256 we decrease its range from `0 to players.length-1` using the mod operaator 342 | 343 | - This selects an index for us and we use that index to retrieve the winner from the players array 344 | - It send all the ether in the contract to the winner and emits a `GameEnded event` 345 | 346 | - Now we would install `dotenv` package to be able to import the env file and use it in our config. Open up a terminal pointing at`hardhat-tutorial` directory and execute this command 347 | 348 | ```bash 349 | npm install dotenv 350 | ``` 351 | 352 | - Now create a `.env` file in the `hardhat-tutorial` folder and add the following lines, use the instructions in the comments to get your `ALCHEMY_API_KEY_URL`, `MUMBAI_PRIVATE_KEY` and `POLYGONSCAN_KEY`.If you dont have Mumbai on MetaMask, you can follow [this](https://portal.thirdweb.com/guides/get-matic-on-polygon-mumbai-testnet-faucet) to add it to your MetaMask, make sure that the account from which you get your mumbai private key is funded with mumbai matic, you can get some from [here](https://faucet.polygon.technology/). 353 | 354 | ```bash 355 | // Go to https://www.alchemyapi.io, sign up, create 356 | // a new App in its dashboard and select the network as Mumbai, and replace "add-the-alchemy-key-url-here" with its key url 357 | ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here" 358 | 359 | // Replace this private key with your Mumbai account private key 360 | // To export your private key from Metamask, open Metamask and 361 | // go to Account Details > Export Private Key 362 | // Be aware of NEVER putting real Ether into testing accounts 363 | MUMBAI_PRIVATE_KEY="add-the-mumbai-private-key-here" 364 | 365 | // Go to https://polygonscan.com/, sign up, on your account overview page, 366 | // click on `API Keys`, add a new API key and copy the 367 | // `API Key Token` 368 | POLYGONSCAN_KEY="add-the-polygonscan-api-token-here" 369 | ``` 370 | 371 | - Now open the hardhat.config.js file, we will add the `mumbai` network here so that we can deploy our contract to mumbai and an `etherscan` object so that we can verify our contract on `polygonscan`. Replace all the lines in the `hardhat.config.js` file with the given below lines. 372 | 373 | ```javascript 374 | require("@nomicfoundation/hardhat-toolbox"); 375 | require("dotenv").config({ path: ".env" }); 376 | require("@nomiclabs/hardhat-etherscan"); 377 | 378 | const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL; 379 | 380 | const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY; 381 | 382 | const POLYGONSCAN_KEY = process.env.POLYGONSCAN_KEY; 383 | 384 | module.exports = { 385 | solidity: "0.8.9", 386 | networks: { 387 | mumbai: { 388 | url: ALCHEMY_API_KEY_URL, 389 | accounts: [MUMBAI_PRIVATE_KEY], 390 | }, 391 | }, 392 | etherscan: { 393 | apiKey: { 394 | polygonMumbai: POLYGONSCAN_KEY, 395 | }, 396 | }, 397 | }; 398 | ``` 399 | 400 | - Create a new folder named as `constants` and inside that add a new file named `index.js`. Add these lines to the `index.js` file: 401 | 402 | ```javascript 403 | const { ethers, BigNumber } = require("hardhat"); 404 | 405 | const LINK_TOKEN = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB"; 406 | const VRF_COORDINATOR = "0x8C7382F9D8f56b33781fE506E897a4F1e2d17255"; 407 | const KEY_HASH = 408 | "0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4"; 409 | const FEE = ethers.utils.parseEther("0.0001"); 410 | module.exports = { LINK_TOKEN, VRF_COORDINATOR, KEY_HASH, FEE }; 411 | ``` 412 | 413 | The values we got for this are from [here](https://docs.chain.link/docs/vrf-contracts/v1/#polygon-matic-mumbai-testnet) and are already provided to us by Chainlink 414 | 415 | - Lets deploy the contract to `mumbai` network. Create a new file, or replace the default existing one, named `deploy.js` under the `scripts` folder. 416 | 417 | ```javascript 418 | const { ethers } = require("hardhat"); 419 | require("dotenv").config({ path: ".env" }); 420 | require("@nomiclabs/hardhat-etherscan"); 421 | const { FEE, VRF_COORDINATOR, LINK_TOKEN, KEY_HASH } = require("../constants"); 422 | 423 | async function main() { 424 | /* 425 | A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, 426 | so randomWinnerGame here is a factory for instances of our RandomWinnerGame contract. 427 | */ 428 | const randomWinnerGame = await ethers.getContractFactory("RandomWinnerGame"); 429 | // deploy the contract 430 | const deployedRandomWinnerGameContract = await randomWinnerGame.deploy( 431 | VRF_COORDINATOR, 432 | LINK_TOKEN, 433 | KEY_HASH, 434 | FEE 435 | ); 436 | 437 | await deployedRandomWinnerGameContract.deployed(); 438 | 439 | // print the address of the deployed contract 440 | console.log( 441 | "Verify Contract Address:", 442 | deployedRandomWinnerGameContract.address 443 | ); 444 | 445 | console.log("Sleeping....."); 446 | // Wait for etherscan to notice that the contract has been deployed 447 | await sleep(30000); 448 | 449 | // Verify the contract after deploying 450 | await hre.run("verify:verify", { 451 | address: deployedRandomWinnerGameContract.address, 452 | constructorArguments: [VRF_COORDINATOR, LINK_TOKEN, KEY_HASH, FEE], 453 | }); 454 | } 455 | 456 | function sleep(ms) { 457 | return new Promise((resolve) => setTimeout(resolve, ms)); 458 | } 459 | 460 | // Call the main function and catch if there is any error 461 | main() 462 | .then(() => process.exit(0)) 463 | .catch((error) => { 464 | console.error(error); 465 | process.exit(1); 466 | }); 467 | ``` 468 | 469 | - Compile the contract, open up a terminal pointing at`hardhat-tutorial` directory and execute this command 470 | 471 | ```bash 472 | npx hardhat compile 473 | ``` 474 | 475 | - To deploy, open up a terminal pointing at `hardhat-tutorial` directory and execute this command 476 | 477 | ```bash 478 | npx hardhat run scripts/deploy.js --network mumbai 479 | ``` 480 | 481 | - It should have printed a link to mumbai polygonscan, your contract is now verified. Click on polygonscan link and interact with your contract there. 482 | 483 | - Lets play the game on polygonscan now 484 | 485 | - In your terminal they should have printed a link to your contract if not then go to [Mumbai Polygon Scan](https://mumbai.polygonscan.com/) and search for your contract address, it should be verified 486 | 487 | - We will now fund this contract with some Chainlink so that we can request randomness, go to [Polygon Faucet](https://faucet.polygon.technology/) and select Link from the dropdown and enter your contract's address 488 | 489 | ![](https://i.imgur.com/HmrQYPs.png) 490 | 491 | - Now connect your wallet to the [Mumbai Polygon Scan](https://mumbai.polygonscan.com/) by clicking on `Connect To Web3`. Make sure your account has some mumbai matic tokens 492 | 493 | ![](https://i.imgur.com/9hOUYgs.png) 494 | 495 | - Once connected, It will look like this 496 | 497 | ![](https://user-images.githubusercontent.com/60979345/159246773-45fdb862-20dd-4992-8323-45add21bb04e.png) 498 | 499 | - Then enter some values in the startGame function and click on Write button 500 | 501 | ![](https://i.imgur.com/ULPV0lh.png) 502 | 503 | 504 | 505 | ![](https://i.imgur.com/Dd9d59J.png) 506 | 507 | - Now you can make your address join the game 508 | Also NOTE: The value I entered here is 10 WEI because that was the value of entry fee that I specified but because join game accepts ether and not WEI, I had to convert 10 WEI into ether. You can also convert your entry fee into ether using [eth converter](https://eth-converter.com/) 509 | 510 | - Now refresh the page and connect a new wallet which has some matic, so that you can make another player join 511 | Note: I set my max players to 2 so it will select the winner after I make another address join the game 512 | 513 | - If you go to your events tab now and keep refreshing (It takes some time for the `VRFCoordinator` to call the `fullFillRandomness` function because it has to get the data from the external world) at one point you will be able to see an event which says `GameEnded` 514 | - From the dropdown convert `Hex` to an `Address` for the first value within the GameEnded event because that is the address of the winner 515 | 516 | 517 | ![](https://i.imgur.com/DdQpsHL.png) 518 | 519 | Boom its done 🚀 520 | 521 | You now know how to play this game. In the next tutorial we will create a UI for this and will learn how to track these events using code itself. 522 | 523 | Lets goo 🚀🚀 524 | 525 | --- 526 | -------------------------------------------------------------------------------- /hardhat-tutorial/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | #Hardhat files 8 | cache 9 | artifacts 10 | -------------------------------------------------------------------------------- /hardhat-tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Basic Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, a sample script that deploys that contract, and an example of a task implementation, which simply lists the available accounts. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat accounts 9 | npx hardhat compile 10 | npx hardhat clean 11 | npx hardhat test 12 | npx hardhat node 13 | node scripts/sample-script.js 14 | npx hardhat help 15 | ``` 16 | -------------------------------------------------------------------------------- /hardhat-tutorial/constants/index.js: -------------------------------------------------------------------------------- 1 | const { ethers, BigNumber } = require("hardhat"); 2 | 3 | const LINK_TOKEN = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB"; 4 | const VRF_COORDINATOR = "0x8C7382F9D8f56b33781fE506E897a4F1e2d17255"; 5 | const KEY_HASH = 6 | "0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4"; 7 | const FEE = ethers.utils.parseEther("0.0001"); 8 | module.exports = { LINK_TOKEN, VRF_COORDINATOR, KEY_HASH, FEE }; 9 | -------------------------------------------------------------------------------- /hardhat-tutorial/contracts/Greeter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "hardhat/console.sol"; 5 | 6 | contract Greeter { 7 | string private greeting; 8 | 9 | constructor(string memory _greeting) { 10 | console.log("Deploying a Greeter with greeting:", _greeting); 11 | greeting = _greeting; 12 | } 13 | 14 | function greet() public view returns (string memory) { 15 | return greeting; 16 | } 17 | 18 | function setGreeting(string memory _greeting) public { 19 | console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); 20 | greeting = _greeting; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /hardhat-tutorial/contracts/RandomWinnerGame.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/utils/Strings.sol"; 7 | import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; 8 | 9 | contract RandomWinnerGame is VRFConsumerBase, Ownable { 10 | 11 | //Chainlink variables 12 | // The amount of LINK to send with the request 13 | uint256 public fee; 14 | // ID of public key against which randomness is generated 15 | bytes32 public keyHash; 16 | 17 | // Address of the players 18 | address[] public players; 19 | //Max number of players in one game 20 | uint8 maxPlayers; 21 | // Variable to indicate if the game has started or not 22 | bool public gameStarted; 23 | // the fees for entering the game 24 | uint256 entryFee; 25 | // current game id 26 | uint256 public gameId; 27 | 28 | // emitted when the game starts 29 | event GameStarted(uint256 gameId, uint8 maxPlayers, uint256 entryFee); 30 | // emitted when someone joins a game 31 | event PlayerJoined(uint256 gameId, address player); 32 | // emitted when the game ends 33 | event GameEnded(uint256 gameId, address winner,bytes32 requestId); 34 | 35 | /** 36 | * constructor inherits a VRFConsumerBase and initiates the values for keyHash, fee and gameStarted 37 | * @param vrfCoordinator address of VRFCoordinator contract 38 | * @param linkToken address of LINK token contract 39 | * @param vrfFee the amount of LINK to send with the request 40 | * @param vrfKeyHash ID of public key against which randomness is generated 41 | */ 42 | constructor(address vrfCoordinator, address linkToken, 43 | bytes32 vrfKeyHash, uint256 vrfFee) 44 | VRFConsumerBase(vrfCoordinator, linkToken) { 45 | keyHash = vrfKeyHash; 46 | fee = vrfFee; 47 | gameStarted = false; 48 | } 49 | 50 | /** 51 | * startGame starts the game by setting appropriate values for all the variables 52 | */ 53 | function startGame(uint8 _maxPlayers, uint256 _entryFee) public onlyOwner { 54 | // Check if there is a game already running 55 | require(!gameStarted, "Game is currently running"); 56 | // empty the players array 57 | delete players; 58 | // set the max players for this game 59 | maxPlayers = _maxPlayers; 60 | // set the game started to true 61 | gameStarted = true; 62 | // setup the entryFee for the game 63 | entryFee = _entryFee; 64 | gameId += 1; 65 | emit GameStarted(gameId, maxPlayers, entryFee); 66 | } 67 | 68 | /** 69 | joinGame is called when a player wants to enter the game 70 | */ 71 | function joinGame() public payable { 72 | // Check if a game is already running 73 | require(gameStarted, "Game has not been started yet"); 74 | // Check if the value sent by the user matches the entryFee 75 | require(msg.value == entryFee, "Value sent is not equal to entryFee"); 76 | // Check if there is still some space left in the game to add another player 77 | require(players.length < maxPlayers, "Game is full"); 78 | // add the sender to the players list 79 | players.push(msg.sender); 80 | emit PlayerJoined(gameId, msg.sender); 81 | // If the list is full start the winner selection process 82 | if(players.length == maxPlayers) { 83 | getRandomWinner(); 84 | } 85 | } 86 | 87 | /** 88 | * fulfillRandomness is called by VRFCoordinator when it receives a valid VRF proof. 89 | * This function is overrided to act upon the random number generated by Chainlink VRF. 90 | * @param requestId this ID is unique for the request we sent to the VRF Coordinator 91 | * @param randomness this is a random unit256 generated and returned to us by the VRF Coordinator 92 | */ 93 | function fulfillRandomness(bytes32 requestId, uint256 randomness) internal virtual override { 94 | // We want out winnerIndex to be in the length from 0 to players.length-1 95 | // For this we mod it with the player.length value 96 | uint256 winnerIndex = randomness % players.length; 97 | // get the address of the winner from the players array 98 | address winner = players[winnerIndex]; 99 | // send the ether in the contract to the winner 100 | (bool sent,) = winner.call{value: address(this).balance}(""); 101 | require(sent, "Failed to send Ether"); 102 | // Emit that the game has ended 103 | emit GameEnded(gameId, winner,requestId); 104 | // set the gameStarted variable to false 105 | gameStarted = false; 106 | } 107 | 108 | /** 109 | * getRandomWinner is called to start the process of selecting a random winner 110 | */ 111 | function getRandomWinner() private returns (bytes32 requestId) { 112 | // LINK is an internal interface for Link token found within the VRFConsumerBase 113 | // Here we use the balanceOF method from that interface to make sure that our 114 | // contract has enough link so that we can request the VRFCoordinator for randomness 115 | require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK"); 116 | // Make a request to the VRF coordinator. 117 | // requestRandomness is a function within the VRFConsumerBase 118 | // it starts the process of randomness generation 119 | return requestRandomness(keyHash, fee); 120 | } 121 | 122 | // Function to receive Ether. msg.data must be empty 123 | receive() external payable {} 124 | 125 | // Fallback function is called when msg.data is not empty 126 | fallback() external payable {} 127 | } -------------------------------------------------------------------------------- /hardhat-tutorial/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-waffle"); 2 | require("dotenv").config({ path: ".env" }); 3 | 4 | const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL; 5 | 6 | const MUMBAI_PRIVATE_KEY = process.env.MUMBAI_PRIVATE_KEY; 7 | 8 | const POLYGONSCAN_KEY = process.env.POLYGONSCAN_KEY; 9 | 10 | module.exports = { 11 | solidity: "0.8.4", 12 | networks: { 13 | mumbai: { 14 | url: ALCHEMY_API_KEY_URL, 15 | accounts: [MUMBAI_PRIVATE_KEY], 16 | }, 17 | }, 18 | etherscan: { 19 | apiKey: { 20 | polygonMumbai: POLYGONSCAN_KEY, 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /hardhat-tutorial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@nomiclabs/hardhat-ethers": "^2.0.5", 14 | "@nomiclabs/hardhat-etherscan": "^3.0.1", 15 | "@nomiclabs/hardhat-waffle": "^2.0.2", 16 | "chai": "^4.3.6", 17 | "ethereum-waffle": "^3.4.0", 18 | "ethers": "^5.5.4", 19 | "hardhat": "^2.8.4" 20 | }, 21 | "dependencies": { 22 | "@chainlink/contracts": "^0.4.0", 23 | "@openzeppelin/contracts": "^4.5.0", 24 | "dotenv": "^16.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /hardhat-tutorial/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | require("dotenv").config({ path: ".env" }); 3 | require("@nomiclabs/hardhat-etherscan"); 4 | const { FEE, VRF_COORDINATOR, LINK_TOKEN, KEY_HASH } = require("../constants"); 5 | 6 | async function main() { 7 | /* 8 | A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts, 9 | so randomWinnerGame here is a factory for instances of our RandomWinnerGame contract. 10 | */ 11 | const randomWinnerGame = await ethers.getContractFactory("RandomWinnerGame"); 12 | // deploy the contract 13 | const deployedRandomWinnerGameContract = await randomWinnerGame.deploy( 14 | VRF_COORDINATOR, 15 | LINK_TOKEN, 16 | KEY_HASH, 17 | FEE 18 | ); 19 | 20 | await deployedRandomWinnerGameContract.deployed(); 21 | 22 | // print the address of the deployed contract 23 | console.log( 24 | "Verify Contract Address:", 25 | deployedRandomWinnerGameContract.address 26 | ); 27 | 28 | console.log("Sleeping....."); 29 | // Wait for etherscan to notice that the contract has been deployed 30 | await sleep(50000); 31 | 32 | // Verify the contract after deploying 33 | await hre.run("verify:verify", { 34 | address: deployedRandomWinnerGameContract.address, 35 | constructorArguments: [VRF_COORDINATOR, LINK_TOKEN, KEY_HASH, FEE], 36 | }); 37 | } 38 | 39 | function sleep(ms) { 40 | return new Promise((resolve) => setTimeout(resolve, ms)); 41 | } 42 | 43 | // Call the main function and catch if there is any error 44 | main() 45 | .then(() => process.exit(0)) 46 | .catch((error) => { 47 | console.error(error); 48 | process.exit(1); 49 | }); 50 | -------------------------------------------------------------------------------- /hardhat-tutorial/scripts/sample-script.js: -------------------------------------------------------------------------------- 1 | // We require the Hardhat Runtime Environment explicitly here. This is optional 2 | // but useful for running the script in a standalone fashion through `node