smartcontractkit / chainlink

node of the decentralized oracle network, bridging on and off-chain computation
https://chain.link
MIT License
6.83k stars 1.65k forks source link

[DEVEL] Fulfillrandomwords not being called by V2 Coordinator #13684

Closed reddigszymon closed 1 week ago

reddigszymon commented 1 week ago

Description I am trying to request a random number from VRF V2 on the sepolia testnet. Locally when mocking the work of the coordinator everything worked smoothly. When i deployed my contract to the testnet, the coordinator never responds by calling the fulfillrandomwords function. I can see the subscription being created, funded and the random word request being sent with the correct parameters (i analyzed events). But unfortunately the coordinator never responds with a random number.

Your Environment Solidity version 0.8.4

Basic Information I am using a UUPS pattern with my smart contract. I scouted chainlinks past issues and found the VRFConsumerBaseV2Upgradeable implementation which seemed to work fine locally.

It looks like this:

``

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

abstract contract VRFConsumerBaseV2Upgradeable is Initializable { error OnlyCoordinatorCanFulfill(address have, address want);

address private vrfCoordinator;

function __VRFConsumerBaseV2Upgradeable_init(address _vrfCoordinator) internal onlyInitializing {
    vrfCoordinator = _vrfCoordinator;
}

function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal virtual;

function rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) external {
    if (msg.sender != vrfCoordinator) {
        revert OnlyCoordinatorCanFulfill(msg.sender, vrfCoordinator);
    }
    fulfillRandomWords(requestId, randomWords);
}

}

``

My smart contract looks like this:

`` // SPDX-License-Identifier: MIT pragma solidity ^0.8.4;

import "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; import "./VRFConsumerBaseV2Upgradeable.sol"; import "./DisputeManagementContract.sol"; import "./VotingContract.sol";

contract DisputeResolution is OwnableUpgradeable, VRFConsumerBaseV2Upgradeable, UUPSUpgradeable { // ============= STRUCTS ============

struct InitParams {
    address owner;
    address stakingContract;
    address vrfCoordinator;
    address votingContract;
    address disputeManagementContract;
    bytes32 keyHash;
    uint32 callbackGasLimit;
    uint16 requestConfirmations;
    uint32 numWords;
    uint256 minStakeRequired;
    uint256 maxVoters;
    address link_token_contract;
}

// ============= VARIABLES ============

LinkTokenInterface LINKTOKEN;
IStakingRewards public stakingContract;
IDDRS public ddrsContract;
VRFCoordinatorV2Interface COORDINATOR;
VotingContract public votingContract;
DisputeManagementContract public disputeManagementContract;
bytes32 public keyHash;
uint32 public callbackGasLimit;
uint16 public requestConfirmations;
uint32 public numWords;
uint256 public minStakeRequired;
uint256 public votingPeriod;
uint256 public maxVoters;
uint64 public s_subscriptionId;

// ============= MAPPINGS ============

mapping(uint256 => uint256) private requestIdToServiceId;
mapping(uint256 => uint256) private serviceIdToRequestId;
mapping(uint256 => uint256) public requestIdToRandomWord;
mapping(uint256 => uint256) public serviceIdToRefundPercentage;

// ============= EVENTS ============

event DisputeFinalized(uint256 indexed requestId);
event DisputeOpened(uint256 indexed serviceId, string description);
event ProofSubmitted(uint256 indexed serviceId, string proofDescription, string[] proofIPFSHashes);
event AddedConsumer(uint64 subscriptionId, address contractAddress);

// ============= MODIFIERS =============

modifier onlyDDRS() {
    require(msg.sender == address(ddrsContract), "Caller is not the ddrs contract");
    _;
}

// ============= PROXY FUNCTIONS ============

function initialize(InitParams memory params) public initializer {
    __VRFConsumerBaseV2Upgradeable_init(params.vrfCoordinator);
    __Ownable_init(params.owner);
    __UUPSUpgradeable_init();
    transferOwnership(params.owner);
    stakingContract = IStakingRewards(params.stakingContract);
    COORDINATOR = VRFCoordinatorV2Interface(params.vrfCoordinator);
    LINKTOKEN = LinkTokenInterface(params.link_token_contract);
    votingContract = VotingContract(params.votingContract);
    disputeManagementContract = DisputeManagementContract(params.disputeManagementContract);
    keyHash = params.keyHash;
    callbackGasLimit = params.callbackGasLimit;
    requestConfirmations = params.requestConfirmations;
    numWords = params.numWords;
    minStakeRequired = params.minStakeRequired;
    votingPeriod = 5 minutes;
    maxVoters = params.maxVoters;

    createNewSubscription();
}

function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

function setContracts(address _ddrsContract, address _votingContract, address _disputeManagementContract) external onlyOwner {
    ddrsContract = IDDRS(_ddrsContract);
    votingContract = VotingContract(_votingContract);
    disputeManagementContract = DisputeManagementContract(_disputeManagementContract);
}

function setDDRSContract(address _ddrsContract) external onlyOwner {
    ddrsContract = IDDRS(_ddrsContract);
}

// ============= MAIN FUNCTIONS ============

/**
 * @notice Submits proof for a given service.
 * @param serviceId The ID of the service for which proof is being submitted.
 * @param proofDescription A description of the proof.
 * @param proofIPFSHashes An array of IPFS hashes for the proof.
 */
function submitProof(uint256 serviceId, string calldata proofDescription, string[] calldata proofIPFSHashes) external {
    disputeManagementContract.submitProof(serviceId, msg.sender, proofDescription, proofIPFSHashes);
    emit ProofSubmitted(serviceId, proofDescription, proofIPFSHashes);
}

/**
 * @notice Opens a dispute for a given service.
 * @param serviceId The ID of the service for which the dispute is being opened.
 * @param disputeDescription A description of the dispute.
 * @param proofIPFSHashes An array of IPFS hashes for the proof related to the dispute.
 */
function openDispute(uint256 serviceId, string calldata disputeDescription, string[] calldata proofIPFSHashes) external {
    disputeManagementContract.openDispute(serviceId, msg.sender, disputeDescription, proofIPFSHashes);
    emit DisputeOpened(serviceId, disputeDescription);
}

/**
 * @notice Initializes dispute parameters for a given service.
 * @dev This function can only be called by the DDRS contract.
 * @param serviceId The ID of the service for which dispute parameters are being initialized.
 * @param timeToUploadProof The time allowed to upload proof.
 * @param timeForUserToOpenDispute The time allowed for a user to open a dispute.
 */
function initializeDisputeParameters(uint256 serviceId, uint256 timeToUploadProof, uint256 timeForUserToOpenDispute) public onlyDDRS {
    disputeManagementContract.initializeDisputeParameters(serviceId, timeToUploadProof, timeForUserToOpenDispute);
}

/**
 * @notice Closes voting and requests VRF for a given service dispute.
 * @dev This function can only be called by the owner. Requires the dispute to be proof submitted, opened, and not finalized.
 * @param serviceId The ID of the service for which voting is being closed and VRF is being requested.
 */
function closeVotingAndRequestVRF(uint256 serviceId) external onlyOwner {
    // DisputeManagementContract.ServiceDispute memory dispute = disputeManagementContract.getServiceDispute(serviceId);
    // require(dispute.proofSubmitted, "No proof submitted");
    // require(dispute.disputeOpened && !dispute.disputeFinalized && block.timestamp > dispute.proofTimestamp + votingPeriod, "Cannot finalize");
    // require(serviceIdToRequestId[serviceId] == 0, "Already closed");

    // address[] memory voters = votingContract.getAllVoters(serviceId);
    // require(voters.length > 0, "No votes casted");

    uint256 requestId = requestRandomnessForVoters(serviceId);
    // requestIdToServiceId[requestId] = serviceId;
    serviceIdToRequestId[serviceId] = requestId;
}

/**
 * @notice Requests randomness for voters for a given service.
 * @param serviceId The ID of the service for which randomness is being requested.
 * @return requestId The ID of the randomness request.
 */
function requestRandomnessForVoters(uint256 serviceId) internal returns (uint256 requestId) {
    requestId = COORDINATOR.requestRandomWords(keyHash, s_subscriptionId, requestConfirmations, callbackGasLimit, numWords);
    requestIdToServiceId[requestId] = serviceId;
}

/**
 * @notice Callback function for fulfilling random words request.
 * @param requestId The ID of the randomness request.
 * @param randomWords The array of random words generated.
 */
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
    requestIdToRandomWord[requestId] = randomWords[0];
    // uint256 serviceId = requestIdToServiceId[requestId];
    emit DisputeFinalized(1234);
    // disputeManagementContract.finalizeDispute(serviceId);
}

/**
 * @notice Processes the voters for a given service dispute.
 * @dev This function can only be called by the owner. Ensures the dispute is proof submitted, opened, finalized, and not already processed.
 * @param serviceId The ID of the service
 */
function processVoters(uint256 serviceId) public onlyOwner {
    uint256 requestId = serviceIdToRequestId[serviceId];
    DisputeManagementContract.ServiceDispute memory dispute = disputeManagementContract.getServiceDispute(serviceId);

    require(dispute.proofSubmitted, "No proof submitted");
    require(dispute.disputeOpened, "Dispute not opened");
    require(dispute.disputeFinalized, "Dispute not finalized");
    require(block.timestamp > dispute.proofTimestamp + votingPeriod, "Voting period not passed");
    require(!dispute.disputeFinalizedAndProcessed, "Already processed");

    uint256 randomWord = requestIdToRandomWord[requestId];

    address[] memory voters = votingContract.getAllVoters(serviceId);

    uint256 numVoters = voters.length > maxVoters ? maxVoters : voters.length;

    // Ensure numVoters is at least 5
    require(numVoters >= 5, "Not enough voters to process");

    // Adjust numVoters to the closest 5, 9, 13, or 17
    if (numVoters > 17) {
        numVoters = 17;
    } else if (numVoters > 13) {
        numVoters = 13;
    } else if (numVoters > 9) {
        numVoters = 9;
    } else if (numVoters > 5) {
        numVoters = 5;
    }

    address[] memory selectedVoters = selectRandomVoters(voters, numVoters, randomWord);

    votingContract.storeSelectedVoters(serviceId, selectedVoters);

    uint256 refundPercentage = votingContract.tallyFinalRefundVotes(serviceId);

    serviceIdToRefundPercentage[serviceId] = refundPercentage;

    disputeManagementContract.markDisputeAsCompleted(serviceId);

    emit DisputeFinalized(requestId);
}

/**
 * @notice Selects random voters from a list of voters.
 * @param voters The list of voters.
 * @param numVoters The number of voters to select.
 * @param randomness The random value used for selection.
 * @return selectedVoters The list of selected voters.
 */
function selectRandomVoters(address[] memory voters, uint256 numVoters, uint256 randomness) internal pure returns (address[] memory) {
    address[] memory selectedVoters = new address[](numVoters);
    uint256 votersLength = voters.length;
    address[] memory tempVoters = new address[](votersLength);

    // Copy the voters to a temporary array
    for (uint256 i = 0; i < votersLength; i++) {
        tempVoters[i] = voters[i];
    }

    uint256 remainingVoters = votersLength;
    for (uint256 i = 0; i < numVoters; i++) {
        uint256 randomIndex = randomness % remainingVoters;
        selectedVoters[i] = tempVoters[randomIndex];

        // Move the last element to the place of the selected element
        tempVoters[randomIndex] = tempVoters[remainingVoters - 1];
        remainingVoters--;

        // Update randomness for the next iteration
        randomness = uint256(keccak256(abi.encode(randomness)));
    }
    return selectedVoters;
}

function createNewSubscription() private onlyOwner {
    s_subscriptionId = COORDINATOR.createSubscription();
    // Add this contract as a consumer of its own subscription.
    COORDINATOR.addConsumer(s_subscriptionId, address(this));
    emit AddedConsumer(s_subscriptionId, address(this));
}

function topUpSubscription(uint256 amount) external onlyOwner {
    LINKTOKEN.transferAndCall(address(COORDINATOR), amount, abi.encode(s_subscriptionId));
}

function addConsumer(address consumerAddress) external onlyOwner {
    // Add a consumer contract to the subscription.
    COORDINATOR.addConsumer(s_subscriptionId, consumerAddress);
}

// ============= GETTER FUNCTIONS ============

/**
 * @notice Returns the dispute statuses for a given service.
 * @param serviceId The ID of the service for which dispute statuses are being retrieved.
 * @return statuses An array of boolean values representing dispute statuses [disputeOpened, disputeFinalized, disputeFinalizedAndProcessed].
 */
function getDisputeStatuses(uint256 serviceId) public view returns (bool[] memory) {
    DisputeManagementContract.ServiceDispute memory dispute = disputeManagementContract.getServiceDispute(serviceId);
    bool[] memory statuses = new bool[](3);
    statuses[0] = dispute.disputeOpened;
    statuses[1] = dispute.disputeFinalized;
    statuses[2] = dispute.disputeFinalizedAndProcessed;

    return statuses;
}

/**
 * @notice Returns the final refund percentage for a given service.
 * @param serviceId The ID of the service for which the final refund percentage is being retrieved.
 * @return refundPercentage The final refund percentage.
 */
function getFinalRefundPercentage(uint256 serviceId) public view returns (uint256) {
    return serviceIdToRefundPercentage[serviceId];
}

/**
 * @notice Returns the dispute details for a given service.
 * @param serviceId The ID of the service for which dispute details are being retrieved.
 * @return dispute The dispute details for the given service.
 */
function getDispute(uint256 serviceId) public view returns (DisputeManagementContract.ServiceDispute memory) {
    DisputeManagementContract.ServiceDispute memory dispute = disputeManagementContract.getServiceDispute(serviceId);
    return dispute;
}

}

``

Straight away as i deploy the contract i fund it with 3 LINK and call the topUpSubscription function to have enough link to fund the VRF. I can confirm that indeed the coordinator receives the link, as i checked on sepolia etherscan.

Examples of my contract deployed:

https://sepolia.etherscan.io/address/0xA6fE2621935187fFF4172B11587a0E4D677Fd529 https://sepolia.etherscan.io/address/0xBAEec702A34e14329831e37ff345E50E07eeedB0

Steps to Reproduce Call the closeVotingAndRequestVRF function with any serviceId, it will successfully send a request to the coordinator, but the fulfillRandomWords function will never get called.

Additional Information

I spent literally over 2 days on this issue, trying all sort of stuff. Nothing seems to work.

reddigszymon commented 1 week ago

I will also add links to some of the subscriptions i created. All of them have a "pending request" that never ends for whatever reason.

https://vrf.chain.link/sepolia/11950 https://vrf.chain.link/sepolia/11949 https://vrf.chain.link/sepolia/11948

reddigszymon commented 1 week ago

Okay, so it seems like depositing 3 LINK as it stated in the documentation is way off. After depositing 25 LINK it is working now. If you have a similiar issue and you are sure your code is correct, just try to put more link into the subscription.