You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Chainlink VRFConsumerBaseV2订阅模式时fulfillRandomWords()异常求助

问题分析与修复方案

核心问题拆解

  1. fulfillRandomWords无法接收VRF返回值

    • 抽奖逻辑顺序错误:getRandomWinner中请求VRF后立刻将持NFT最多的地址加入获奖者并标记抽奖结束,此时VRF回调还未执行,后续回调逻辑完全失效。
    • Callback Gas Limit过低:当前设置的callbackGasLimit=100000不足以支撑遍历获奖者数组、调用外部NFT合约等操作,导致VRF Coordinator回调失败。
    • 潜在配置问题:需确认VRF订阅已将当前合约添加为消费者,且链上VRF Coordinator地址、KeyHash与部署环境匹配。
  2. 重复获奖者过滤异常

    • 死循环风险:当前while循环使用同一个随机数重复计算tokenId,若该tokenId对应的地址已在获奖者列表中,会无限循环无法退出。
    • 数组遍历效率低:用数组winners检查重复的时间复杂度是O(n),当获奖者数量较多时会消耗大量Gas。

修复后的合约代码

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

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

interface INFTContract {
    function totalSupply() external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);
    function balanceOf(address owner) external view returns (uint256);
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);
}

contract Raffle is VRFConsumerBaseV2, ConfirmedOwner {
    struct RequestStatus {
        bool fulfilled;
        bool exists;
        uint256[] randomWords;
    }
    
    struct AddressMintCount {
        address addr;
        uint256 count;
    }
    
    mapping(uint256 => RequestStatus) public s_requests;
    mapping(address => bool) public isWinner; // 新增:快速判断是否已获奖
    VRFCoordinatorV2Interface COORDINATOR;
    uint64 s_subscriptionId;
    uint256[] public requestIds;
    uint256 public lastRequestId;
    bytes32 immutable keyHash;
    address public immutable linkToken;
    INFTContract public nftContract;
    uint256 public minMints;
    uint256 public maxMints;
    bool public raffleEnded;
    address[] public winners;
    uint32 callbackGasLimit = 200000; // 提高Gas Limit
    uint16 requestConfirmations = 3;
    uint32 numWords = 2;
    address vrfCoordinator = 0xbd13f08b8352A3635218ab9418E340c60d6Eb418;
    address linkAddress = 0xfaFedb041c0DD4fA2Dc0d87a6B0979Ee6FA7af5F;

    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(uint256 requestId, uint256[] randomWords);
    event WinnerAdded(address winner);

    constructor(
        uint64 subscriptionId, 
        address _nftContractAddress
    )
        VRFConsumerBaseV2(vrfCoordinator)
        ConfirmedOwner(msg.sender)
    {
        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_subscriptionId = subscriptionId;
        keyHash = 0x121a143066e0f2f08b620784af77cccb35c6242460b4a8ee251b4b416abaebd4;
        linkToken = linkAddress;
        nftContract = INFTContract(_nftContractAddress);
        minMints = 1;
        maxMints = 30;
        raffleEnded = false;
    }

    function isRaffleReady() public view returns (bool) {
        return nftContract.totalSupply() >= maxMints;
    }

    function printWinners() public view returns (address[] memory){
        return winners;
    }

    // 修正:仅发起VRF请求,不在此处处理获奖者
    function startRaffle() public onlyOwner returns (uint256 requestId) {
        require(!raffleEnded, "Raffle already ended");
        require(isRaffleReady(), "Raffle not ready");
        requestId = requestRandomWords();
        return requestId;
    }

    function requestRandomWords() public onlyOwner returns (uint256 requestId) {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            s_subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
        s_requests[requestId] = RequestStatus({
            randomWords: new uint256[](0),
            exists: true,
            fulfilled: false
        });
        requestIds.push(requestId);
        lastRequestId = requestId;
        emit RequestSent(requestId, numWords);
        return requestId;
    }

    function fulfillRandomWords(
        uint256 _requestId,
        uint256[] memory _randomWords
    ) internal override {
        require(s_requests[_requestId].exists, "request not found");
        require(!s_requests[_requestId].fulfilled, "request already fulfilled");
        require(!raffleEnded, "Raffle already ended");

        s_requests[_requestId].fulfilled = true;
        s_requests[_requestId].randomWords = _randomWords;

        // 先添加持NFT最多的地址
        address topHolder = getAddressWithMostMints();
        if (!isWinner[topHolder]) {
            winners.push(topHolder);
            isWinner[topHolder] = true;
            emit WinnerAdded(topHolder);
        }

        // 处理VRF随机选出的获奖者
        uint256 maxAttempts = 10; // 防止死循环的最大尝试次数
        for (uint256 i = 0; i < _randomWords.length; i++) {
            uint256 attempt = 0;
            uint256 result = (_randomWords[i] % maxMints) + minMints;
            address winner = nftContract.ownerOf(result);

            while (isWinner[winner] && attempt < maxAttempts) {
                attempt++;
                // 用当前随机数+尝试次数生成新的tokenId,避免重复
                result = ((_randomWords[i] + attempt) % maxMints) + minMints;
                winner = nftContract.ownerOf(result);
            }

            // 如果找到未获奖的地址,添加到列表
            if (!isWinner[winner]) {
                winners.push(winner);
                isWinner[winner] = true;
                emit WinnerAdded(winner);
            }
        }

        raffleEnded = true; // 所有获奖者处理完后再结束抽奖
        emit RequestFulfilled(_requestId, _randomWords);
    }

    // 修正:用mapping快速判断重复
    function isWinnerDuplicate(address winner) private view returns (bool) {
        return isWinner[winner];
    }

    function getAddressWithMostMints() public view returns (address) {
        address addrWithMostMints = address(0);
        uint256 mostMints = 0;

        uint256 addrMintCountIndex = 0;
        AddressMintCount[200] memory addrMintCounts;

        for (uint256 tokenId = minMints; tokenId <= maxMints; tokenId++) {
            address currentOwner = nftContract.ownerOf(tokenId);

            bool found = false;
            for (uint256 i = 0; i < addrMintCountIndex; i++) {
                if (addrMintCounts[i].addr == currentOwner) {
                    addrMintCounts[i].count++;
                    found = true;
                    break;
                }
            }

            if (!found) {
                addrMintCounts[addrMintCountIndex] = AddressMintCount({
                    addr: currentOwner,
                    count: 1
                });
                addrMintCountIndex++;
            }
        }

        for (uint256 i = 0; i < addrMintCountIndex; i++) {
            if (addrMintCounts[i].count > mostMints) {
                addrWithMostMints = addrMintCounts[i].addr;
                mostMints = addrMintCounts[i].count;
            }
        }

        return addrWithMostMints;
    }

    function getRequestStatus(uint256 _requestId) external view returns (bool fulfilled, uint256[] memory randomWords) {
        require(s_requests[_requestId].exists, "request not found");
        RequestStatus memory request = s_requests[_requestId];
        return (request.fulfilled, request.randomWords);
    }
}

关键修改说明

  1. 调整抽奖流程

    • 将原getRandomWinner拆分为startRaffle,仅负责发起VRF请求,不在请求后直接修改获奖者状态。
    • 所有获奖者(包括持NFT最多的地址)的添加逻辑移到fulfillRandomWords中,确保VRF回调完成后再结束抽奖。
  2. 修复VRF回调问题

    • 提高callbackGasLimit到200000,足够支撑回调内的逻辑执行。
    • 增加raffleEnded判断,防止重复处理回调。
  3. 解决重复获奖者异常

    • 新增isWinner mapping,将重复检查的时间复杂度从O(n)降到O(1),节省Gas并提高效率。
    • 在while循环中加入最大尝试次数maxAttempts,避免死循环;每次尝试用随机数+尝试次数生成新的tokenId,确保能获取新的地址。
  4. 增强事件通知

    • 新增WinnerAdded事件,方便前端监控获奖者添加情况。

内容的提问来源于stack exchange,提问作者Pina

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 13:09:58