admin管理员组文章数量:1277887
I’m building an NFT-based game where two users receive random numbers via Chainlink VRF. The user with the higher number wins and receives the opponent’s NFT from another contract.
The contract successfully generates random numbers and emits the RandomNumberFulfilled event, which I can see on Etherscan. However, the contract doesn’t proceed to the next stage, where the NFT should be transferred to the winner.
Here’s a high-level overview of the flow:
Players commit to the game by depositing their NFTs.
Chainlink VRF generates random numbers for both players.
The contract emits the RandomNumberFulfilled event with the generated numbers.
The contract should determine the winner and transfer the NFT.
The issue occurs after step 3—nothing happens after the RandomNumberFulfilled event is logged.
What I’ve checked so far:
The RandomNumberFulfilled event is emitted, confirming that the VRF callback is working.
Both players have approved the game contract to transfer their NFTs using setApprovalForAll.
The NFT contract’s isApprovedForAll function confirms that the game contract has the necessary permissions.
The callbackGasLimit in the VRF request is set to a sufficient value (500,000).
Questions:
Why might the contract stop executing after emitting the RandomNumberFulfilled event?
Are there any common pitfalls in transferring NFTs after a VRF callback?
How can I debug this issue further?
Here is my full code
pragma solidity ^0.8.20;
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {VRFConsumerBaseV2Plus} from "chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
import {IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
contract NFTgame is VRFConsumerBaseV2Plus {
error NotOwner();
error GameIsNotOpen();
error GameIsFull();
error NotApprovedForNFT1();
error NotApprovedForNFT2();
enum GameState {
OPEN,
IN_PROGRESS
}
//state of the gamers
struct GamerStatus {
uint256 token_ID;
uint256 randomNumber;
address player;
bool isWinner;
bool fulfilled;
}
//state of the game
struct Game {
GameState gameState;
uint256 tokenId1;
uint256 tokenId2;
address player1;
address player2;
uint256 randomNumber1;
uint256 randomNumber2;
bool fulfilled;
bool isWinner1;
bool isWinner2;
bool isDraw;
}
struct RequestInfo {
uint256 gameId;
bool isPlayer1;
}
address public vrfCoordinator;
uint256 public s_subId;
address public s_nftContract;
uint256 public s_gameId;
GameState public s_gameState;
uint256 public s_lastRequestId;
bytes32 private constant KEY_HASH = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae;
uint32 private constant GAS_LIMIT = 2000000;
uint32 private constant NUM_WORDS = 1;
uint16 private constant REQUEST_CONFIRMATIONS = 3;
mapping(uint256 gameId => GamerStatus) public s_gamerStatuses;
mapping(uint256 gameId => Game) s_games;
mapping(uint256 requestId => RequestInfo) public s_requestInfo;
event GameID(uint256 gameId);
event GameResult(address player, uint256 gameID, bool isWinner);
event GameStarted(uint256 gameId, address player1, uint256 tokenId1);
event PlayerJoined(uint256 gameId, address player2, uint256 tokenId2);
event RandomNumberRequested(uint256 requestId, uint256 gameId, bool isPlayer1);
event RandomNumberFulfilled(uint256 requestId, uint256 gameId, uint256 randomNumber);
event GameTie(uint256 gameId);
constructor(address _nftContract, address _vrfCoordinator, uint256 _subId) VRFConsumerBaseV2Plus(_vrfCoordinator) {
s_nftContract = _nftContract;
vrfCoordinator = _vrfCoordinator;
s_subId = _subId;
s_gameId = 0;
}
function startTheGame(uint256 tokenId) external {
// putting your NFT's ID
if (IERC721(s_nftContract).ownerOf(tokenId) != msg.sender) {
revert IERC721Errors.ERC721NonexistentToken(tokenId); // revert if not owner
}
if (s_gameState != GameState.OPEN) {
revert GameIsNotOpen();
}
s_gameState = GameState.IN_PROGRESS;
s_gameId++; // id of your game
s_games[s_gameId] = Game({ //saving information about the game
gameState: GameState.IN_PROGRESS,
tokenId1: tokenId,
tokenId2: 0,
player1: msg.sender,
player2: address(0),
randomNumber1: 0,
randomNumber2: 0,
fulfilled: false,
isWinner1: false,
isWinner2: false,
isDraw: false
});
emit GameStarted(s_gameId, msg.sender, tokenId);
}
function joinTheGame(uint256 gameId, uint256 tokenId) external {
//joining an existing game
if (IERC721(s_nftContract).ownerOf(tokenId) != msg.sender) {
revert IERC721Errors.ERC721NonexistentToken(tokenId); // revert if not owner
}
if (s_gameState != GameState.IN_PROGRESS) {
revert GameIsNotOpen();
}
Game storage game = s_games[gameId]; //creating game object, changing it on storage memory
if (game.tokenId2 != 0) {
revert GameIsFull();
}
game.tokenId2 = tokenId;
game.player2 = msg.sender;
emit PlayerJoined(gameId, msg.sender, tokenId);
requestRandomWords(gameId);
}
function requestRandomWords(uint256 gameId) internal {
//creating 2 requests for 2 random numbers
uint256 requestId1 = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: s_subId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: GAS_LIMIT,
numWords: NUM_WORDS,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
})
);
uint256 requestId2 = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: s_subId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: GAS_LIMIT,
numWords: NUM_WORDS,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
})
);
s_lastRequestId = requestId2;
s_requestInfo[requestId1] = RequestInfo({gameId: gameId, isPlayer1: true});
s_requestInfo[requestId2] = RequestInfo({gameId: gameId, isPlayer1: false});
s_gamerStatuses[requestId1] = GamerStatus({
token_ID: s_games[gameId].tokenId1,
randomNumber: 0,
player: s_games[gameId].player1,
isWinner: false,
fulfilled: false
});
s_gamerStatuses[requestId2] = GamerStatus({
token_ID: s_games[gameId].tokenId2,
randomNumber: 0,
player: s_games[gameId].player2,
isWinner: false,
fulfilled: false
});
emit RandomNumberRequested(requestId1, gameId, true);
emit RandomNumberRequested(requestId2, gameId, false);
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
GamerStatus storage gamerStatus = s_gamerStatuses[requestId];
gamerStatus.randomNumber = randomWords[0];
gamerStatus.fulfilled = true;
RequestInfo memory requestInfo = s_requestInfo[requestId];
Game storage game = s_games[requestInfo.gameId];
if (requestInfo.isPlayer1) {
game.randomNumber1 = randomWords[0];
} else {
game.randomNumber2 = randomWords[0];
}
emit RandomNumberFulfilled(requestId, requestInfo.gameId, randomWords[0]);
if (game.randomNumber1 != 0 && game.randomNumber2 != 0) {
determineWinner(requestInfo.gameId);
}
s_gameState = GameState.OPEN;
}
function determineWinner(uint256 gameId) internal {
Game storage game = s_games[gameId];
if (
!IERC721(s_nftContract).isApprovedForAll(game.player1, address(this))
&& IERC721(s_nftContract).getApproved(game.tokenId1) != address(this)
) {
revert NotApprovedForNFT1();
}
if (
!IERC721(s_nftContract).isApprovedForAll(game.player2, address(this))
&& IERC721(s_nftContract).getApproved(game.tokenId2) != address(this)
) {
revert NotApprovedForNFT2();
}
if (game.randomNumber1 > game.randomNumber2) {
game.isWinner1 = true;
IERC721(s_nftContract).safeTransferFrom(game.player2, game.player1, game.tokenId2);
emit GameResult(game.player1, gameId, true);
} else if (game.randomNumber1 < game.randomNumber2) {
game.isWinner2 = true;
IERC721(s_nftContract).safeTransferFrom(game.player1, game.player2, game.tokenId1);
emit GameResult(game.player2, gameId, true);
} else {
game.isDraw = true;
emit GameTie(gameId);
}
game.fulfilled = true;
emit GameResult(game.isWinner1 ? game.player1 : game.player2, gameId, true);
}
function getSubId() public view returns (uint256) {
return s_subId;
}
function getVrfCoordinator() public view returns (address) {
return vrfCoordinator;
}
function getGameId() public view returns (uint256) {
return s_gameId;
}
function getLatestRequestId() public view returns (uint256) {
return s_lastRequestId;
}
function getGame(uint256 gameId) public view returns (
GameState gameState,
uint256 tokenId1,
uint256 tokenId2,
address player1,
address player2,
uint256 randomNumber1,
uint256 randomNumber2,
bool fulfilled,
bool isWinner1,
bool isWinner2,
bool isDraw
) {
Game storage game = s_games[gameId];
return (
game.gameState,
game.tokenId1,
game.tokenId2,
game.player1,
game.player2,
game.randomNumber1,
game.randomNumber2,
game.fulfilled,
game.isWinner1,
game.isWinner2,
game.isDraw
);
}
}```
Any help or suggestions would be greatly appreciated!
I’m building an NFT-based game where two users receive random numbers via Chainlink VRF. The user with the higher number wins and receives the opponent’s NFT from another contract.
The contract successfully generates random numbers and emits the RandomNumberFulfilled event, which I can see on Etherscan. However, the contract doesn’t proceed to the next stage, where the NFT should be transferred to the winner.
Here’s a high-level overview of the flow:
Players commit to the game by depositing their NFTs.
Chainlink VRF generates random numbers for both players.
The contract emits the RandomNumberFulfilled event with the generated numbers.
The contract should determine the winner and transfer the NFT.
The issue occurs after step 3—nothing happens after the RandomNumberFulfilled event is logged.
What I’ve checked so far:
The RandomNumberFulfilled event is emitted, confirming that the VRF callback is working.
Both players have approved the game contract to transfer their NFTs using setApprovalForAll.
The NFT contract’s isApprovedForAll function confirms that the game contract has the necessary permissions.
The callbackGasLimit in the VRF request is set to a sufficient value (500,000).
Questions:
Why might the contract stop executing after emitting the RandomNumberFulfilled event?
Are there any common pitfalls in transferring NFTs after a VRF callback?
How can I debug this issue further?
Here is my full code
pragma solidity ^0.8.20;
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {VRFConsumerBaseV2Plus} from "chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
import {IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
contract NFTgame is VRFConsumerBaseV2Plus {
error NotOwner();
error GameIsNotOpen();
error GameIsFull();
error NotApprovedForNFT1();
error NotApprovedForNFT2();
enum GameState {
OPEN,
IN_PROGRESS
}
//state of the gamers
struct GamerStatus {
uint256 token_ID;
uint256 randomNumber;
address player;
bool isWinner;
bool fulfilled;
}
//state of the game
struct Game {
GameState gameState;
uint256 tokenId1;
uint256 tokenId2;
address player1;
address player2;
uint256 randomNumber1;
uint256 randomNumber2;
bool fulfilled;
bool isWinner1;
bool isWinner2;
bool isDraw;
}
struct RequestInfo {
uint256 gameId;
bool isPlayer1;
}
address public vrfCoordinator;
uint256 public s_subId;
address public s_nftContract;
uint256 public s_gameId;
GameState public s_gameState;
uint256 public s_lastRequestId;
bytes32 private constant KEY_HASH = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae;
uint32 private constant GAS_LIMIT = 2000000;
uint32 private constant NUM_WORDS = 1;
uint16 private constant REQUEST_CONFIRMATIONS = 3;
mapping(uint256 gameId => GamerStatus) public s_gamerStatuses;
mapping(uint256 gameId => Game) s_games;
mapping(uint256 requestId => RequestInfo) public s_requestInfo;
event GameID(uint256 gameId);
event GameResult(address player, uint256 gameID, bool isWinner);
event GameStarted(uint256 gameId, address player1, uint256 tokenId1);
event PlayerJoined(uint256 gameId, address player2, uint256 tokenId2);
event RandomNumberRequested(uint256 requestId, uint256 gameId, bool isPlayer1);
event RandomNumberFulfilled(uint256 requestId, uint256 gameId, uint256 randomNumber);
event GameTie(uint256 gameId);
constructor(address _nftContract, address _vrfCoordinator, uint256 _subId) VRFConsumerBaseV2Plus(_vrfCoordinator) {
s_nftContract = _nftContract;
vrfCoordinator = _vrfCoordinator;
s_subId = _subId;
s_gameId = 0;
}
function startTheGame(uint256 tokenId) external {
// putting your NFT's ID
if (IERC721(s_nftContract).ownerOf(tokenId) != msg.sender) {
revert IERC721Errors.ERC721NonexistentToken(tokenId); // revert if not owner
}
if (s_gameState != GameState.OPEN) {
revert GameIsNotOpen();
}
s_gameState = GameState.IN_PROGRESS;
s_gameId++; // id of your game
s_games[s_gameId] = Game({ //saving information about the game
gameState: GameState.IN_PROGRESS,
tokenId1: tokenId,
tokenId2: 0,
player1: msg.sender,
player2: address(0),
randomNumber1: 0,
randomNumber2: 0,
fulfilled: false,
isWinner1: false,
isWinner2: false,
isDraw: false
});
emit GameStarted(s_gameId, msg.sender, tokenId);
}
function joinTheGame(uint256 gameId, uint256 tokenId) external {
//joining an existing game
if (IERC721(s_nftContract).ownerOf(tokenId) != msg.sender) {
revert IERC721Errors.ERC721NonexistentToken(tokenId); // revert if not owner
}
if (s_gameState != GameState.IN_PROGRESS) {
revert GameIsNotOpen();
}
Game storage game = s_games[gameId]; //creating game object, changing it on storage memory
if (game.tokenId2 != 0) {
revert GameIsFull();
}
game.tokenId2 = tokenId;
game.player2 = msg.sender;
emit PlayerJoined(gameId, msg.sender, tokenId);
requestRandomWords(gameId);
}
function requestRandomWords(uint256 gameId) internal {
//creating 2 requests for 2 random numbers
uint256 requestId1 = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: s_subId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: GAS_LIMIT,
numWords: NUM_WORDS,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
})
);
uint256 requestId2 = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: s_subId,
requestConfirmations: REQUEST_CONFIRMATIONS,
callbackGasLimit: GAS_LIMIT,
numWords: NUM_WORDS,
extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
})
);
s_lastRequestId = requestId2;
s_requestInfo[requestId1] = RequestInfo({gameId: gameId, isPlayer1: true});
s_requestInfo[requestId2] = RequestInfo({gameId: gameId, isPlayer1: false});
s_gamerStatuses[requestId1] = GamerStatus({
token_ID: s_games[gameId].tokenId1,
randomNumber: 0,
player: s_games[gameId].player1,
isWinner: false,
fulfilled: false
});
s_gamerStatuses[requestId2] = GamerStatus({
token_ID: s_games[gameId].tokenId2,
randomNumber: 0,
player: s_games[gameId].player2,
isWinner: false,
fulfilled: false
});
emit RandomNumberRequested(requestId1, gameId, true);
emit RandomNumberRequested(requestId2, gameId, false);
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
GamerStatus storage gamerStatus = s_gamerStatuses[requestId];
gamerStatus.randomNumber = randomWords[0];
gamerStatus.fulfilled = true;
RequestInfo memory requestInfo = s_requestInfo[requestId];
Game storage game = s_games[requestInfo.gameId];
if (requestInfo.isPlayer1) {
game.randomNumber1 = randomWords[0];
} else {
game.randomNumber2 = randomWords[0];
}
emit RandomNumberFulfilled(requestId, requestInfo.gameId, randomWords[0]);
if (game.randomNumber1 != 0 && game.randomNumber2 != 0) {
determineWinner(requestInfo.gameId);
}
s_gameState = GameState.OPEN;
}
function determineWinner(uint256 gameId) internal {
Game storage game = s_games[gameId];
if (
!IERC721(s_nftContract).isApprovedForAll(game.player1, address(this))
&& IERC721(s_nftContract).getApproved(game.tokenId1) != address(this)
) {
revert NotApprovedForNFT1();
}
if (
!IERC721(s_nftContract).isApprovedForAll(game.player2, address(this))
&& IERC721(s_nftContract).getApproved(game.tokenId2) != address(this)
) {
revert NotApprovedForNFT2();
}
if (game.randomNumber1 > game.randomNumber2) {
game.isWinner1 = true;
IERC721(s_nftContract).safeTransferFrom(game.player2, game.player1, game.tokenId2);
emit GameResult(game.player1, gameId, true);
} else if (game.randomNumber1 < game.randomNumber2) {
game.isWinner2 = true;
IERC721(s_nftContract).safeTransferFrom(game.player1, game.player2, game.tokenId1);
emit GameResult(game.player2, gameId, true);
} else {
game.isDraw = true;
emit GameTie(gameId);
}
game.fulfilled = true;
emit GameResult(game.isWinner1 ? game.player1 : game.player2, gameId, true);
}
function getSubId() public view returns (uint256) {
return s_subId;
}
function getVrfCoordinator() public view returns (address) {
return vrfCoordinator;
}
function getGameId() public view returns (uint256) {
return s_gameId;
}
function getLatestRequestId() public view returns (uint256) {
return s_lastRequestId;
}
function getGame(uint256 gameId) public view returns (
GameState gameState,
uint256 tokenId1,
uint256 tokenId2,
address player1,
address player2,
uint256 randomNumber1,
uint256 randomNumber2,
bool fulfilled,
bool isWinner1,
bool isWinner2,
bool isDraw
) {
Game storage game = s_games[gameId];
return (
game.gameState,
game.tokenId1,
game.tokenId2,
game.player1,
game.player2,
game.randomNumber1,
game.randomNumber2,
game.fulfilled,
game.isWinner1,
game.isWinner2,
game.isDraw
);
}
}```
Any help or suggestions would be greatly appreciated!
Share
edited Feb 24 at 10:27
6kim6krueger6
asked Feb 24 at 10:16
6kim6krueger66kim6krueger6
11 bronze badge
1 Answer
Reset to default 0I've deployed your contract on Avalanche Fuji (by changing the value of KEY_HASH
). It's available at 0x9A487c954Ae676C4a280d6DCd7E6D526606628D4. You can see that the GameResult
event is being emitted and showing in the transaction logs.
Then, I tested on my end, and it's working completely fine, provided that the approvals from the NFT contract are being performed correctly on behalf of both the players.
In your case, it's most probably failing while calling the determineWinner
function. You can confirm the same by checking the History tab corresponding to your VRF Subscription on the VRF Dashboard, which shows two fulfillments (one for each player), it might be failing in case of the second fulfillment that's triggering the determineWinner
function call.
And, the reason of the failure could be the revert due to either of these if
conditions:
if (
!IERC721(s_nftContract).isApprovedForAll(game.player1, address(this))
&& IERC721(s_nftContract).getApproved(game.tokenId1) != address(this)
) {
revert NotApprovedForNFT1();
}
if (
!IERC721(s_nftContract).isApprovedForAll(game.player2, address(this))
&& IERC721(s_nftContract).getApproved(game.tokenId2) != address(this)
) {
revert NotApprovedForNFT2();
}
It requires:
isApprovedForAll
==true
→ Means theNFTgame
contract should be approved to transfer all NFTs of the player.getApproved(tokenId)
==address(this)
→ Means theNFTgame
contract should be approved to transfer this specific NFT (corresponding totokenId
).
NOTE: Even, calling the setApprovalForAll
using both the players would not make the getApproved(tokenId)
return the address of the NFTgame
contract.
Therefore, in order to pass these conditions:
Using the account corresponding to
player1
, you need to call:setApprovalForAll
: passing the address ofNFTgame
contract andtrue
.approve
: passing the address ofNFTgame
contract and thetokenId
of the NFT owned byplayer1
(using which theplayer1
is supposed to participate in the game).
Using the account corresponding to
player2
, you need to call:setApprovalForAll
: passing the address ofNFTgame
contract andtrue
.approve
: passing the address ofNFTgame
contract and thetokenId
of the NFT owned byplayer2
(using which theplayer2
is supposed to participate in the game).
If you miss any of these 4 steps, then the determineWinner
function call would revert.
P.S. For simplicity, you can modify both of your if
conditions in the determineWinner
function to check either for isApprovedForAll
or getApproved
by replacing &&
with ||
, like:
if (
!IERC721(s_nftContract).isApprovedForAll(game.player1, address(this))
|| IERC721(s_nftContract).getApproved(game.tokenId1) != address(this)
) {
revert NotApprovedForNFT1();
}
if (
!IERC721(s_nftContract).isApprovedForAll(game.player2, address(this))
|| IERC721(s_nftContract).getApproved(game.tokenId2) != address(this)
) {
revert NotApprovedForNFT2();
}
本文标签: ethereumProblem with transfering ERC721 token in an external contractStack Overflow
版权声明:本文标题:ethereum - Problem with transfering ERC-721 token in an external contract - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741278494a2369869.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论