2

I've spent many weeks on this project trying to figure out why "Chainlink Keepers" is not working. I can successfully deploy and verify the contract on Rinkeby. Then i do the following... 1.)Fund and Register the consumer contract successfully (VFR) 2.)Fund and Register the Keepers successfully and see my contact listed as active for upkeep 3.)I go to "write contract" tab on etherscan and enter my own lottery via the enterLottery function. The tx is successful and visible on Etherscan.

Now after I enter this lottery, the keepers is supposed to be set up that after a certain amount of block.timestamps pass it will choose a winner of the lottery and pay out the winner. Even if there is only one player it is set to choose a winner. Unfortuantley keepers is not "upkeeping" and I am not seeing any upkeep in the history. Can anyone who is versed with contract development and ChainLink VRF & keppers spot what the issue may be. Both the contract and the deployment scripts are below. Would greatly appreciate any help you can provide....

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/KeeperCompatibleInterface.sol";

//Goals for our lotttery contract...
//We want users to be able to enter the lottery (paying some fee to enter)
//Users can pick a random number (verifiably random)
//Winner to be selected every X minutes or at some frequency we decide on
//We want everything to be completely automated, not requiring manual maintenance
//Must use a chainlink oracle = Randomness (automated execution) Chainlink keepers

error Raffle_NotEnoughETHEntered();
error Raffle_TransferFailed();
error Raffle_NotOpen();
error Raffle_UpkeepNotNeeded(
    uint256 currentBalance,
    uint256 numPlayers,
    uint256 raffleState
);

/** @title A sample Lottery Contract
 *  @author AroundTheBlock7
 *  @notice This contract is for creating an untamperable decentralized smart contract
 *  @dev This implements Chainlink VRF v2 and Chainlink Keepers
 */

contract LotteryVRF is VRFConsumerBaseV2, KeeperCompatibleInterface {
    enum RaffleState {
        OPEN, //0
        CALCULATING //1
    }

    //State Variables
    VRFCoordinatorV2Interface private immutable i_vrfCoordinator;
    uint256 public immutable i_entranceFee;
    address payable[] public s_players;
    bytes32 private immutable i_gasLane;
    uint64 private immutable i_subscriptionId;
    uint16 private constant REQUEST_CONFIRMATIONS = 3;
    uint32 private immutable i_callbackGasLimit;
    uint32 private constant NUM_WORDS = 1;

    //Lottery variables
    address private s_recentWinner;
    RaffleState private s_raffleState;
    uint256 private s_lastTimeStamp;
    uint256 private immutable i_interval;

    event LotteryEnter(address indexed player);
    event RequestedLotteryWinner(uint256 indexed requestId);
    event WinnerPicked(address indexed winner);

    constructor(
        address vrfCoordinatorV2,
        uint256 entranceFee,
        bytes32 gasLane,
        uint64 subscriptionId,
        uint32 callbackGasLimit,
        uint256 interval
    ) VRFConsumerBaseV2(vrfCoordinatorV2) {
        i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinatorV2);
        i_entranceFee = entranceFee;
        i_gasLane = gasLane;
        i_subscriptionId = subscriptionId;
        i_callbackGasLimit = callbackGasLimit;
        s_raffleState = RaffleState.OPEN;
        s_lastTimeStamp = block.timestamp;
        i_interval = interval;
    }

    function enterLottery() public payable {
        //require (msg.value > i_entranceFee, "Not enough ETH!") Instead of this, we can use the if and revert which is cheaper
        if (msg.value < i_entranceFee) {
            revert Raffle_NotEnoughETHEntered();
        }
        if (s_raffleState != RaffleState.OPEN) {
            revert Raffle_NotOpen();
        }
        s_players.push(payable(msg.sender));
        emit LotteryEnter(msg.sender);
    }

    //This is the function that the Chainlink Keeper Nodes call. It is run offchain! 
    //They look for the `upkeepNeeded' to return true.
    //The following should be true in order to return true....
    //1.) Our time interval should have passed
    //2.) The lottery should have at least 1 player and have some ETH
    //3.) Our subscription is funded with LINK
    //4.) The lottery should be in an "open" state.
    //The input bytes memory checkData allows us to input any data we want. We wont need for this so we can take out checkData
    //We also don't need performData in the return statement which woulc allow us to do other stuff. We want bool upkeepNeeded!
    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 = (isOpen && timePassed && hasPlayers && hasBalance);
        return (upkeepNeeded, "0x0");
    }

    //This function interacts interacts VRFConsumerBaseV2 Contract/Oracle.
    //If checkUpKeep returns true, than the node automatically calls performUpkeep here.
    //When this is executed by the node it returns a random number & triggers the fulfillRandomWords 
    //Initally we called this function "requestRandomWinner", but after we incorporated keepers and wrote...
    //...the "checkUpkeep" function we renamed this function to performUpkeep and gave it proper inputs (bytes calldata performData)
    function performUpkeep(bytes calldata /* performData */) external override {
        //Remember upkeepNeeded and performData were the 2 things returned to us in checkUpkeep function above
        //We pass in upkeepNeeded here but we do not need performData so leave that out
        (bool upkeepNeeded, ) = checkUpkeep("");
        if (!upkeepNeeded) {
            revert Raffle_UpkeepNotNeeded(
                address(this).balance,
                s_players.length,
                uint256(s_raffleState)
            );
        }
        //We want to make sure we set the RaffleState to calculating so to avoid new entries while we pick a winner
        s_raffleState = RaffleState.CALCULATING;
        uint256 requestId = i_vrfCoordinator.requestRandomWords(
            i_gasLane,
            i_subscriptionId,
            REQUEST_CONFIRMATIONS,
            i_callbackGasLimit,
            NUM_WORDS
        );

        emit RequestedLotteryWinner(requestId);
    }

    //This function is filled by the VRFCoordinator via the VRFCoordinatorV2Interface.
    //This is automatically called after the requestRandomWinner/performUpkeep function is triggered. The VRFCoordinator fills the request here.
    function fulfillRandomWords(
        uint256,
        /*requestId */
        uint256[] memory randomWords
    ) internal override {
        uint256 indexOfWinner = randomWords[0] % s_players.length;
        address payable recentWinner = s_players[indexOfWinner];
        s_recentWinner = recentWinner;
        //After we pick the winner we want to set the RaffleState to open again and reset the players array!
        s_raffleState = RaffleState.OPEN;
        s_players = new address payable[](0); //resets the array
        //We also want to reset the timestamp here so to keep things running smooth with the interval and picking next winner
        s_lastTimeStamp = block.timestamp;
        (bool success, ) = recentWinner.call{value: address(this).balance}("");
        //require(success, etc.) chepaer way to do it...
        if (!success) {
            revert Raffle_TransferFailed();
        }
        emit WinnerPicked(recentWinner);
    }

    function getEntranceFee() public view returns (uint256) {
        return i_entranceFee;
    }

    function getPlayer(uint256 index) public view returns (address) {
        return s_players[index];
    }

    function getRecentWinner() public view returns (address) {
        return s_recentWinner;
    }

    function getRaffleState() public view returns (RaffleState) {
        return s_raffleState;
    }

    //When retreiving constant variables in storage we can use "pure" instead of "view" for visibility
    function getNumWords() public pure returns (uint256) {
        return NUM_WORDS;
    }

    function getNumberOfPlayers() public view returns (uint256) {
        return s_players.length;
    }

    function getLatestTimeStamp() public view returns (uint256) {
        return s_lastTimeStamp;
    }

    //Again, can use "pure" instead of "view" when returning constants
    function getRequestConfirmations() public pure returns (uint256) {
        return REQUEST_CONFIRMATIONS;
    }
}

Here is deployment script. I also have a mock contract and mock deployment script but since i am deploying to Rinkeby i did not inlude those as those are only for the local deployments via hardhat.

const { network, ethers } = require("hardhat")
const { developmentChains, networkConfig } = require("../helper-hardhat-config")
const { verify } = require("../utils/verify")

const VRF_SUB_FUND_AMOUNT = ethers.utils.parseEther("30")

module.exports = async function ({ getNamedAccounts, deployments }) {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()
    const chainId = network.config.chainId
    let vrfCoordinatorV2Address, subscriptionId

    if (developmentChains.includes(network.name)) {
        const vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock")
        vrfCoordinatorV2Address = vrfCoordinatorV2Mock.address
        const transactionResponse = await vrfCoordinatorV2Mock.createSubscription()
        const transactionReceipt = await transactionResponse.wait(1)
        subscriptionId = transactionReceipt.events[0].args.subId
        //Fund the subscription
        //Usually you'd need the link token on a real network 
        await vrfCoordinatorV2Mock.fundSubscription(subscriptionId, VRF_SUB_FUND_AMOUNT)
    } else {
        vrfCoordinatorV2Address = networkConfig[chainId]["vrfCoordinatorV2"]
        subscriptionId = networkConfig[chainId]["subscriptionId"]
    }

    const entranceFee = networkConfig[chainId]["entranceFee"]
    const gasLane = networkConfig[chainId]["gasLane"]
    const callbackGasLimit = networkConfig[chainId]["callbackGasLimit"]
    const interval = networkConfig[chainId]["interval"]

    const args = [vrfCoordinatorV2Address, entranceFee, gasLane, subscriptionId, callbackGasLimit, interval]
    const lotteryVRF = await deploy("LotteryVRF", {
        from: deployer,
        args: args,
        log: true,
        waitConfirmations: network.config.blockConfirmations || 1,
    })

    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        await verify(lotteryVRF.address, args)
    }

    log("--------------------------------------------")
}
module.exports.tags = ["all", "lotteryVRF"]

Here is helper-hardhat-config.js file...

const { ethers } = require("hardhat")

//Note all these below are being derived from our constructor. These are the inputs needed upon deployment.
const networkConfig = {
    4: {
        name: "rinkeby",
        vrfCoordinatorV2: "0x6168499c0cFfCaCD319c818142124B7A15E857ab",
        entranceFee: ethers.utils.parseEther("0.01"),
        gasLane: "0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc",
        subscriptionId: "0",
        callbackGasLimit: "500000", // 500,000 
        interval: "30",
    },
    //We don't need to put the vrfCoordinator below because we are using our mock on hardhat
    //For gasLane we can put the same as above or any filler as we'll be using the mock so it won't matter 
    31337: {
        name: "hardhat",
        entranceFee: ethers.utils.parseEther("0.01"),
        gasLane: "0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc",
        subscriptionId: "0",
        callbackGasLimit: "500000", // 500,000 
        interval: "30",
    },
}
const developmentChains = ["hardhat", "localhost"]

module.exports = {
    networkConfig,
    developmentChains,
}
3
  • can you share the address you deployed the contract to on rinkeby?
    – ryanrhall
    Commented Aug 10, 2022 at 21:11
  • Hey i ended up figuring this out. I think it was becuase i had the subscriptionId in my helper-hardhat-config.js file set to 0. For anyone looking at this or running inot the same problem make sure you get the subscriptionId and include it in your deployment file. Then deploy the contract. I was getting the subscriptionid after i already deployed and my helper-hardhat-config file did not deploy with the SubscriptionId. I guess that needs to be in there before you deploy. Thanks again for responding here and offering to look into it.
    – Blockpain
    Commented Aug 11, 2022 at 2:12
  • That makes sense -- i was trying to figure out why your upkeeps history was blank, I see you've deleted the link now.
    – ZeusLawyer
    Commented Aug 11, 2022 at 3:10

1 Answer 1

0

The PerformUpkeep Tx appears to have failed and reverted (see here: https://rinkeby.etherscan.io/address/0x4b3499aac5A0f96E2DFB6e31c28f7fd92DFA3a31). If you click on the tx hash on the left it will open up a more detailed view.

The Tx appeared to have originated from your wallet - are you calling performUpkeep? Thats what the Tx seems to indicate(https://rinkeby.etherscan.io/tx/0xaf66dec83393f27f4112c6d1f0fdee094d564f253ef7a6ba57b0d6663a6368e3)

Another reason why the TX may have failed is that the gaslimit is too low (this can happen due to the variations in the exchange rate between LINK and ETH etc). So perhaps double or triple the gas limit and see if that works.

1
  • Hey thanks for the response. Appreciate you checking into it. Yeah that peformUpkeep failutre was me manually trying to force the upKeep function myself to see if it would work if I manually triggered. But as you know this is suppossed to be automatically triggered after enteryLottery function with Keepers automatically upkeeping. Anyway long story short, I believe the error lied in the fact that i did not deploy my contract with the subscriptionId attached. Can't be 100% but i think thats what ultimately fixed the issue. Thanks again.
    – Blockpain
    Commented Aug 11, 2022 at 17:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.