diff --git a/contracts/Raffle.sol b/contracts/Raffle.sol index 68ca4e6..41d0faf 100644 --- a/contracts/Raffle.sol +++ b/contracts/Raffle.sol @@ -12,22 +12,62 @@ pragma solidity ^0.8.7; ///// UPDATE IMPORTS TO V2.5 ///// import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; +import "@chainlink/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol"; +import "@chainlink/contracts/src/v0.8/automation/interfaces/AutomationCompatibleInterface.sol"; +import "hardhat/console.sol"; error Raffle__NotEnoughETHEntered(); +error Raffle__TransferFailed(); +error Raffle__RaffleNotOpen(); +error Raffle__IntervalNotPassed(); +error Raffle__UpkeepNotNeeded(uint256 currentBalance, uint256 numPlayers, uint256 raffleState); + +contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface { + /* Type declarations */ + enum RaffleState { + OPEN, + CALCULATING + } // uint256 0 = OPEN, 1 = CALCULATING -contract Raffle is VRFConsumerBaseV2Plus { /* State Variables */ - uint256 private immutable i_entranceFee; + // Chainlink VRF Variable + IVRFCoordinatorV2Plus private immutable i_vrfCoordinator; + bytes32 private immutable i_gasLane; + uint256 private immutable i_subscriptionId; + uint32 private immutable i_callbackGasLimit; + uint16 private constant REQUEST_CONFIRMATIONS = 3; + uint32 private constant NUM_WORDS = 1; + + // Lottery Variables address payable[] private s_players; + RaffleState private s_raffleState; + address private s_recentWinner; + uint256 private s_lastTimeStamp; + uint256 private immutable i_interval; + uint256 private immutable i_entranceFee; /* Event */ + event RequestedRaffleWinner(uint256 indexed requestId); event RaffleEnter(address indexed player); + event WinnerPicked(address indexed player); + /* Function */ constructor( address vrfCoordinatorV2, - uint256 entranceFee + uint64 subscriptionId, + bytes32 gasLane, // keyHash + uint256 interval, + uint256 entranceFee, + uint32 callbackGasLimit ) VRFConsumerBaseV2Plus(vrfCoordinatorV2) { + i_vrfCoordinator = IVRFCoordinatorV2Plus(vrfCoordinatorV2); + i_gasLane = gasLane; + i_interval = interval; + i_subscriptionId = subscriptionId; i_entranceFee = entranceFee; + s_raffleState = RaffleState.OPEN; + s_lastTimeStamp = block.timestamp; + i_callbackGasLimit = callbackGasLimit; } function enterRaffle() public payable { @@ -35,8 +75,13 @@ contract Raffle is VRFConsumerBaseV2Plus { if (msg.value < i_entranceFee) { revert Raffle__NotEnoughETHEntered(); } + if (s_raffleState != RaffleState.OPEN) { + revert Raffle__RaffleNotOpen(); + } s_players.push(payable(msg.sender)); - // Events - Update + // Emit an event when we update a dynamic array or mapping + // Named events with the function name reversed + emit RaffleEnter(msg.sender); } function requestRandomWinner() external returns (uint256 requestId) { @@ -61,14 +106,128 @@ contract Raffle is VRFConsumerBaseV2Plus { * Consumer address - 0x22eec58ce2cee446051337d71d59c89cb004d1c7 * Admin Approval Contract address - 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B */ - // Request Random Number - // + /* + // Deprecated, Now V2.5 insteads of V2 + i_vrfCoordinator.requestRandomWords( + i_gasLane, + i_subscriptionId, + REQUEST_CONFIRMATIONS, + i_callbackGasLimit, + NUM_WORDS + ); + */ + if ((block.timestamp - s_lastTimeStamp) <= i_interval) { + revert Raffle__IntervalNotPassed(); + } + s_raffleState = RaffleState.CALCULATING; + + // Prepare the ExtraArgs structure + VRFV2PlusClient.ExtraArgsV1 memory extraArgsV1 = VRFV2PlusClient.ExtraArgsV1({ + nativePayment: true // Set to true or false depending on your use case + }); + + // Encode ExtraArgs to bytes + bytes memory extraArgs = VRFV2PlusClient._argsToBytes(extraArgsV1); + + // Prepare the RandomWordsRequest structure + VRFV2PlusClient.RandomWordsRequest memory req = VRFV2PlusClient.RandomWordsRequest({ + keyHash: i_gasLane, + subId: i_subscriptionId, + requestConfirmations: REQUEST_CONFIRMATIONS, + callbackGasLimit: i_callbackGasLimit, + numWords: NUM_WORDS, + extraArgs: extraArgs + }); + + // Request random words using the prepared structure + requestId = i_vrfCoordinator.requestRandomWords(req); + emit RequestedRaffleWinner(requestId); + } + + /** + * @dev This is the function that the Chainlink Keeper nodes call + * they look for `upkeepNeeded` to return True. + * the following should be true for this to return true: + * 1. The time interval has passed between raffle runs. + * 2. The lottery is open. + * 3. The contract has ETH. + * 4. Implicity, your subscription is funded with LINK. + */ + function checkUpkeep( + bytes memory /* checkData */ + ) public view override returns (bool upkeepNeeded, bytes memory /* performData */) { + bool isOpen = RaffleState.OPEN == s_raffleState; + bool timePassed = ((block.timestamp - s_lastTimeStamp) > i_interval); + bool hasPlayers = s_players.length > 0; + bool hasBalance = address(this).balance > 0; + upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers); + return (upkeepNeeded, "0x0"); // can we comment this out? + } + + /** + * @dev Once `checkUpkeep` is returning `true`, this function is called + * and it kicks off a Chainlink VRF call to get a random winner. + */ + function performUpkeep(bytes calldata /* performData */) external override { + (bool upkeepNeeded, ) = checkUpkeep(""); + // require(upkeepNeeded, "Upkeep not needed"); + if (!upkeepNeeded) { + revert Raffle__UpkeepNotNeeded( + address(this).balance, + s_players.length, + uint256(s_raffleState) + ); + } + s_raffleState = RaffleState.CALCULATING; + + // Prepare the ExtraArgs structure + VRFV2PlusClient.ExtraArgsV1 memory extraArgsV1 = VRFV2PlusClient.ExtraArgsV1({ + nativePayment: true // Set to true or false depending on your use case + }); + + // Encode ExtraArgs to bytes + bytes memory extraArgs = VRFV2PlusClient._argsToBytes(extraArgsV1); + + // Prepare the RandomWordsRequest structure + VRFV2PlusClient.RandomWordsRequest memory req = VRFV2PlusClient.RandomWordsRequest({ + keyHash: i_gasLane, + subId: i_subscriptionId, + requestConfirmations: REQUEST_CONFIRMATIONS, + callbackGasLimit: i_callbackGasLimit, + numWords: NUM_WORDS, + extraArgs: extraArgs + }); + + // Request random words using the prepared structure + uint256 requestId = i_vrfCoordinator.requestRandomWords(req); + + // Quiz... is this redundant? + emit RequestedRaffleWinner(requestId); } function fulfillRandomWords( - uint256 requestId, + uint256 /* requestId */, uint256[] calldata randomWords - ) internal override {} + ) internal override { + // s_players size 10 + // randomNumber 202 + // 202 % 10 ? what's doesn't divide evenly into 202? + // 20 * 10 = 200 + // 2 + // 202 % 10 = 2 + uint256 indexOfWinner = randomWords[0] % s_players.length; + address payable recentWinner = s_players[indexOfWinner]; + s_recentWinner = recentWinner; + s_players = new address payable[](0); + s_raffleState = RaffleState.OPEN; + s_lastTimeStamp = block.timestamp; + (bool success, ) = recentWinner.call{value: address(this).balance}(""); + // require(success, "Transfer failed"); + if (!success) { + revert Raffle__TransferFailed(); + } + emit WinnerPicked(recentWinner); + } function getEntranceFee() public view returns (uint256) { return i_entranceFee; @@ -77,4 +236,8 @@ contract Raffle is VRFConsumerBaseV2Plus { function getPlayer(uint256 index) public view returns (address) { return s_players[index]; } + + function getRecentWinner() public view returns (address) { + return s_recentWinner; + } } diff --git a/contracts/test/VRFCoordinatorV2Mock.sol b/contracts/test/VRFCoordinatorV2Mock.sol new file mode 100644 index 0000000..7d6b8de --- /dev/null +++ b/contracts/test/VRFCoordinatorV2Mock.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@chainlink/contracts/src/v0.8/vrf/mocks/VRFCoordinatorV2_5Mock.sol"; diff --git a/deploy/00-deploy-mocks.js b/deploy/00-deploy-mocks.js new file mode 100644 index 0000000..8fd3fb5 --- /dev/null +++ b/deploy/00-deploy-mocks.js @@ -0,0 +1,30 @@ +const { network } = require("hardhat"); + +const BASE_FEE = "250000000000000000"; // 0.25 is this the premium in LINK? +const GAS_PRICE_LINK = 1e9; // link per gas, is this the gas lane? // 0.000000001 LINK per gas + +module.exports = async ({ getNamedAccounts, deployments }) => { + const { deploy, log } = deployments; + const { deployer } = await getNamedAccounts(); + const chainId = network.config.chainId; + // If we are on a local development network, we need to deploy mocks! + if (chainId == 31337) { + log("Local network detected! Deploying mocks..."); + await deploy("VRFCoordinatorV2Mock", { + from: deployer, + log: true, + args: [BASE_FEE, GAS_PRICE_LINK], + }); + + log("Mocks Deployed!"); + log("----------------------------------------------------------"); + log( + "You are deploying to a local network, you'll need a local network running to interact", + ); + log( + "Please run `yarn hardhat console --network localhost` to interact with the deployed smart contracts!", + ); + log("----------------------------------------------------------"); + } +}; +module.exports.tags = ["all", "mocks"]; diff --git a/deploy/01-deploy-raffle.js b/deploy/01-deploy-raffle.js new file mode 100644 index 0000000..408786d --- /dev/null +++ b/deploy/01-deploy-raffle.js @@ -0,0 +1,69 @@ +const { network, ethers } = require("hardhat"); +const { + networkConfig, + developmentChains, + VERIFICATION_BLOCK_CONFIRMATIONS, +} = require("../helper-hardhat-config"); +const { verify } = require("../utils/verify"); + +const FUND_AMOUNT = ethers.utils.parseEther("1"); // 1 Ether, or 1e18 (10^18) Wei + +module.exports = async ({ getNamedAccounts, deployments }) => { + const { deploy, log } = deployments; + const { deployer } = await getNamedAccounts(); + const chainId = network.config.chainId; + let vrfCoordinatorV2Address, subscriptionId, vrfCoordinatorV2Mock; + + if (chainId == 31337) { + // create VRFV2 Subscription + vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock"); + vrfCoordinatorV2Address = vrfCoordinatorV2Mock.address; + const transactionResponse = await vrfCoordinatorV2Mock.createSubscription(); + const transactionReceipt = await transactionResponse.wait(); + subscriptionId = transactionReceipt.events[0].args.subId; + // Fund the subscription + // Our mock makes it so we don't actually have to worry about sending fund + await vrfCoordinatorV2Mock.fundSubscription(subscriptionId, FUND_AMOUNT); + } else { + vrfCoordinatorV2Address = networkConfig[chainId]["vrfCoordinatorV2"]; + subscriptionId = networkConfig[chainId]["subscriptionId"]; + } + const waitBlockConfirmations = developmentChains.includes(network.name) + ? 1 + : VERIFICATION_BLOCK_CONFIRMATIONS; + + log("----------------------------------------------------"); + const arguments = [ + vrfCoordinatorV2Address, + subscriptionId, + networkConfig[chainId]["gasLane"], + networkConfig[chainId]["keepersUpdateInterval"], + networkConfig[chainId]["raffleEntranceFee"], + networkConfig[chainId]["callbackGasLimit"], + ]; + const raffle = await deploy("Raffle", { + from: deployer, + args: arguments, + log: true, + waitConfirmations: waitBlockConfirmations, + }); + + // Ensure the Raffle contract is a valid consumer of the VRFCoordinatorV2Mock contract. + if (developmentChains.includes(network.name)) { + const vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock"); + await vrfCoordinatorV2Mock.addConsumer(subscriptionId, raffle.address); + } + + // Verify the deployment + if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) { + log("Verifying..."); + await verify(raffle.address, arguments); + } + + log("Enter lottery with command:"); + const networkName = network.name == "hardhat" ? "localhost" : network.name; + log(`yarn hardhat run scripts/enterRaffle.js --network ${networkName}`); + log("----------------------------------------------------"); +}; + +module.exports.tags = ["all", "raffle"]; diff --git a/helper-hardhat-config.js b/helper-hardhat-config.js new file mode 100644 index 0000000..b68789e --- /dev/null +++ b/helper-hardhat-config.js @@ -0,0 +1,43 @@ +const { ethers } = require("hardhat"); + +const networkConfig = { + default: { + name: "hardhat", + keepersUpdateInterval: "30", + }, + 31337: { + name: "localhost", + subscriptionId: "588", + gasLane: "0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c", // 30 gwei + keepersUpdateInterval: "30", + raffleEntranceFee: ethers.utils.parseEther("0.01"), // 0.01 ETH + callbackGasLimit: "500000", // 500,000 gas + }, + 11155111: { + name: "sepolia", + subscriptionId: "6926", + gasLane: "0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c", // 30 gwei + keepersUpdateInterval: "30", + raffleEntranceFee: ethers.utils.parseEther("0.01"), // 0.01 ETH + callbackGasLimit: "500000", // 500,000 gas + vrfCoordinatorV2: "0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625", + }, + 1: { + name: "mainnet", + keepersUpdateInterval: "30", + }, +}; + +const developmentChains = ["hardhat", "localhost"]; +const VERIFICATION_BLOCK_CONFIRMATIONS = 6; +const frontEndContractsFile = + "../nextjs-smartcontract-lottery-fcc/constants/contractAddresses.json"; +const frontEndAbiFile = "../nextjs-smartcontract-lottery-fcc/constants/abi.json"; + +module.exports = { + networkConfig, + developmentChains, + VERIFICATION_BLOCK_CONFIRMATIONS, + frontEndContractsFile, + frontEndAbiFile, +};