使用Chainlink VRFConsumerBaseV2订阅模式时fulfillRandomWords()异常求助
问题分析与修复方案
核心问题拆解
fulfillRandomWords无法接收VRF返回值
- 抽奖逻辑顺序错误:
getRandomWinner中请求VRF后立刻将持NFT最多的地址加入获奖者并标记抽奖结束,此时VRF回调还未执行,后续回调逻辑完全失效。 - Callback Gas Limit过低:当前设置的
callbackGasLimit=100000不足以支撑遍历获奖者数组、调用外部NFT合约等操作,导致VRF Coordinator回调失败。 - 潜在配置问题:需确认VRF订阅已将当前合约添加为消费者,且链上VRF Coordinator地址、KeyHash与部署环境匹配。
- 抽奖逻辑顺序错误:
重复获奖者过滤异常
- 死循环风险:当前
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); } }
关键修改说明
调整抽奖流程
- 将原
getRandomWinner拆分为startRaffle,仅负责发起VRF请求,不在请求后直接修改获奖者状态。 - 所有获奖者(包括持NFT最多的地址)的添加逻辑移到
fulfillRandomWords中,确保VRF回调完成后再结束抽奖。
- 将原
修复VRF回调问题
- 提高
callbackGasLimit到200000,足够支撑回调内的逻辑执行。 - 增加
raffleEnded判断,防止重复处理回调。
- 提高
解决重复获奖者异常
- 新增
isWinnermapping,将重复检查的时间复杂度从O(n)降到O(1),节省Gas并提高效率。 - 在
while循环中加入最大尝试次数maxAttempts,避免死循环;每次尝试用随机数+尝试次数生成新的tokenId,确保能获取新的地址。
- 新增
增强事件通知
- 新增
WinnerAdded事件,方便前端监控获奖者添加情况。
- 新增
内容的提问来源于stack exchange,提问作者Pina
相关产品推荐
相关产品推荐

