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
Add a comment  | 

1 Answer 1

Reset to default 0

I'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 the NFTgame contract should be approved to transfer all NFTs of the player.

  • getApproved(tokenId) == address(this) → Means the NFTgame contract should be approved to transfer this specific NFT (corresponding to tokenId).

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:

    1. setApprovalForAll: passing the address of NFTgame contract and true.
    2. approve: passing the address of NFTgame contract and the tokenId of the NFT owned by player1 (using which the player1 is supposed to participate in the game).
  • Using the account corresponding to player2, you need to call:

    1. setApprovalForAll: passing the address of NFTgame contract and true.
    2. approve: passing the address of NFTgame contract and the tokenId of the NFT owned by player2 (using which the player2 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