Building Randomness with Chainlink VRF: Part 2

charlesj_dev

Charles Jones

Posted on June 16, 2024

Building Randomness with Chainlink VRF: Part 2

Image description

“In the game of innovation, like in sports, every function plays a crucial role; it’s not just about having star players, but how every part works together seamlessly.”

Overview

Welcome to the digital arena, where the players are smart contracts, and the game is as strategic as any sport you’ve ever seen. Today, we’re diving into the playbook (functions) of our MVP, RandomTeamSelector.sol, dissecting its moves and strategies to understand how it scores points in the blockchain league. If you missed part 1, you can read it here.

Imagine a football coach picking players for a dream team, but instead of athletes, we’re drafting functions, each with a unique role that contributes to the victory of our smart contract. From the kickoff with requestRandomTeamNamesto the final whistle with chooseTeam, we’ll explore how each function interacts, strategizes, and executes plays that would make even the most seasoned sports analysts take notes.

So, lace up your cleats, don your jerseys, and let’s get ready to rumble through the code, play by play, ensuring you understand the Xs and Os of our smart contract’s game plan.

The Draft Pick: Initiating the Random Team Selection

In the grand league of smart contracts, the requestRandomTeamNamesfunction is the first-round draft pick, setting the stage for a fair and unpredictable selection process. It’s the crucial coin toss, the unbiased referee, ensuring that every team manager gets a shot at glory in the blockchain championship.

Here’s the play-by-play:

Positioning the Players: The function takes an address manager as its input, which is akin to selecting the team manager who’s calling the shots.

Checking the Field: It checks if the manager has already made a team selection. If so, it throws a flag on the play with RandomTeamSelector__AlreadySelected(), stopping the action right there.

The Kickoff: If the coast is clear, it makes a call to s_vrfCoordinator.requestRandomWords, which is like passing the ball to the referee (the VRF Coordinator) to ensure a fair and random kickoff.

Setting the Strategy: The function sets up the request for random words with specific parameters like keyHash, subId, and callbackGasLimit, ensuring the play adheres to the game rules.

Executing the Play: Once the request is made, it records the requestId and associates it with the manager, marking the selection as ongoing with SELECTION_ONGOING.

Celebrating the Move: Finally, it announces the play to the spectators (other contracts and listeners) with the SelectionMadeevent.

Let’s break down each line of the requestRandomTeamNames function for clarity:

function requestRandomTeamNames(
    address manager
) public onlyOwner returns (uint256 requestId) {
Enter fullscreen mode Exit fullscreen mode

This line declares the function requestRandomTeamNames, which takes one parameter: the address of the manager. It’s a public function, meaning it can be called by anyone, but with the onlyOwnermodifier, it restricts access so only the contract owner can execute it. The function is set to return a uint256 variable named requestId.

if (s_managerSelections[manager].teamOptions.length > 0) {
        revert RandomTeamSelector__AlreadySelected();
    }
Enter fullscreen mode Exit fullscreen mode

Here, we’re checking if the manager has already selected a team. If the teamOptionsarray for this manager is not empty (length > 0), the function will stop and revert the transaction with a custom error RandomTeamSelector__AlreadySelected.

requestId = s_vrfCoordinator.requestRandomWords(
Enter fullscreen mode Exit fullscreen mode

This line assigns a new requestIdby calling the requestRandomWordsfunction from the s_vrfCoordinator, which is a reference to a Chainlink VRF Coordinator contract that provides secure and verifiable randomness. More specifically, you can see the requestRandomWordsfunction directly in VRFCoordinatorV2.5.sol . It's in the lib folder

/lib/chainlink/contracts/src/v0.8/vrf/dev/VRFCoordinatorV2_5.sol

VRFV2PlusClient.RandomWordsRequest({
            keyHash: s_keyHash,
            subId: s_subscriptionId,
            requestConfirmations: requestConfirmations,
            callbackGasLimit: callbackGasLimit,
            numWords: numWords,
            extraArgs: VRFV2PlusClient._argsToBytes(
                VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
            )
        })
    );
Enter fullscreen mode Exit fullscreen mode

This section is constructing a RandomWordsRequeststruct with various parameters:

keyHash: A unique identifier for the VRF key against which randomness is requested.

subId: The subscription ID for billing purposes.

requestConfirmations: The number of confirmations the VRF Coordinator requires before responding.

callbackGasLimit: The maximum amount of gas to use for the callback.

numWords: The number of random words requested.

extraArgs: Additional arguments, here converted to bytes, indicating no native token payment is required with this request.

s_requestToManager[requestId] = manager;
Enter fullscreen mode Exit fullscreen mode

This line maps the requestIdto the manager’s address, storing the association in a state variable.

s_managerSelections[manager].selectedTeam = SELECTION_ONGOING;
Enter fullscreen mode Exit fullscreen mode

Here, the function sets the selectedTeamstatus to SELECTION_ONGOING for the manager, indicating that the selection process is currently underway.

emit SelectionMade(requestId, manager);
}
Enter fullscreen mode Exit fullscreen mode

Finally, the function emits an event SelectionMadewith the requestIdand manager’s address, signaling to the blockchain that a selection process has been initiated.

This function sets up the initial request for randomness, which is the first step in the process of selecting a random team. It ensures that the selection is fair and hasn’t been tampered with by using a verifiable source of randomness.

The Playmaker: fulfillRandomWords Function

In the playbook of our smart contract, the fulfillRandomWordsfunction is the playmaker, the one who receives the pass (randomness) and makes the strategic moves (assigns team options). Here’s how it orchestrates the play:

Receiving the Pass: The function is triggered by a callback from the VRF Coordinator, carrying the requestIdand an array of randomWords—the unpredictable elements that keep the game fair.

Setting Up the Play: It retrieves the manager’s address associated with the requestIdand prepares an array teamOptionsto hold the potential team picks.

Executing the Strategy: A loop runs through the randomWords, using modulo arithmetic to ensure the team options fall within a specified range (in this case, 1 to 40), representing the pool of available teams.

Recording the Stats: The manager’s selection is updated with these new team options, and the selectedTeamstatus is set to SELECTION_ONGOING, indicating the play is in progress.

Announcing the Outcome: Finally, the SelectionRevealedevent is emitted, broadcasting the manager’s team options to all participants.

This function is the core of the selection process, taking the randomness provided by the VRF and translating it into actionable choices for the manager. It’s the moment when the crowd holds its breath as the playmaker weaves through the opposition, setting up for a score.

Let's take a deeper look at the code.

function fulfillRandomWords(
    uint256 requestId,
    uint256[] calldata randomWords
) internal override {
Enter fullscreen mode Exit fullscreen mode

This function is an internal callback that is called by the VRF Coordinator when randomness is delivered. It overrides a function from the inherited VRFConsumerBaseV2Plus contract. The parameters are the ID of the request for randomness and the array of random numbers provided by the VRF.

 address manager = s_requestToManager[requestId];
Enter fullscreen mode Exit fullscreen mode

This line retrieves the manager’s address that is associated with the given requestIdfrom a mapping called s_requestToManager.

uint256[] memory teamOptions = new uint256;
Enter fullscreen mode Exit fullscreen mode

Here, a new temporary array called teamOptionsis created in memory to store the team IDs that will be generated from the random words.

for (uint256 i = 0; i < numWords; i++) {
        teamOptions[i] = (randomWords[i] % 40) + 1;
    }
Enter fullscreen mode Exit fullscreen mode

This loop goes through each of the random numbers provided, uses modulo operation to ensure each number is within the range of 1 to 40, and assigns the result to the teamOptionsarray.

 s_managerSelections[manager] = ManagerSelection({
        teamOptions: teamOptions,
        selectedTeam: SELECTION_ONGOING
    });

Enter fullscreen mode Exit fullscreen mode

The s_managerSelectionsmapping is updated for the manager with a new ManagerSelectionstruct, which includes the teamOptionsarray and sets the selectedTeamstatus to SELECTION_ONGOING.

emit SelectionRevealed(requestId, teamOptions);
}
Enter fullscreen mode Exit fullscreen mode

Finally, the SelectionRevealedevent is emitted, which includes the requestIdand the teamOptions array, indicating that the selection options have been successfully generated and revealed.

Let’s proceed with the next function, teamName, which retrieves the name of the team chosen by a player.

The Selector: teamName Function

In the arena of our smart contract, the teamNamefunction is like the referee announcing the chosen player to the eager crowd. It’s the moment of truth where the selection is revealed and the anticipation turns into reality.

Play by Play:

The Announcement: This function is called upon to reveal the chosen team’s name, much like a referee would announce the selected player.

The Verification: Before the reveal, it ensures that a selection has indeed been made, akin to checking the roster before making the call.

The Reveal: Once confirmed, it retrieves and announces the team’s name, finalizing the player’s choice in the draft.

Line by Line Code Breakdown:

function teamName(address player) public view returns (string memory) {
Enter fullscreen mode Exit fullscreen mode

This line declares the function teamName, which is a public function that anyone can call. It’s a view function, meaning it doesn’t modify the state on the blockchain. It takes one parameter, the address of the player, and returns a string representing the name of the team.

ManagerSelection storage selection = s_managerSelections[player];
Enter fullscreen mode Exit fullscreen mode

Here, the function retrieves the ManagerSelectionstruct from the s_managerSelectionsmapping for the given player address and stores it in a local variable selection.

if (selection.selectedTeam == 0) {
        revert RandomTeamSelector__SelectionNotMade();
    }

Enter fullscreen mode Exit fullscreen mode

This line checks if the selectedTeamproperty of the selection is 0, which would indicate that no selection has been made. If that’s the case, it reverts the transaction with a custom error RandomTeamSelector__SelectionNotMade.

return getTeamName(selection.selectedTeam);
}
Enter fullscreen mode Exit fullscreen mode

Finally, the function returns the name of the selected team by calling another function getTeamNameand passing the selectedTeamID from the selection.

Moving on to the next function, let’s explore getTeamOptions, which retrieves all the team options available for a manager.

The Strategist: getTeamOptions Function

In the tactical game of our smart contract, the getTeamOptionsfunction is akin to a coach reviewing the playbook. It’s where the manager surveys all possible moves before making the crucial decision.

Play by Play:

Surveying the Field: This function allows the manager to look at all the potential team options at their disposal.

Ensuring Fair Play: It checks to make sure the selection process is still ongoing, akin to verifying that the game hasn’t ended before making a move.

Revealing the Options: Once confirmed, it presents the array of team IDs, laying out all the possible strategies for the manager to consider.

Line by Line Code Breakdown:

function getTeamOptions(
    address manager
) public view returns (uint256[] memory) {
Enter fullscreen mode Exit fullscreen mode

This line declares the function getTeamOptions, which is publicly accessible and doesn’t alter the state on the blockchain. It takes the manager’s address as a parameter and returns an array of unsigned integers, which represent the team IDs.

ManagerSelection storage selection = s_managerSelections[manager];
Enter fullscreen mode Exit fullscreen mode

Here, the function retrieves the ManagerSelectionstruct associated with the manager’s address from the s_managerSelectionsmapping.

if (selection.selectedTeam != SELECTION_ONGOING) {
        revert RandomTeamSelector__NoSelectionOptionsAvailable();
    }
Enter fullscreen mode Exit fullscreen mode

This line checks if the selection process is not ongoing (meaning either not started or already completed). If so, it reverts the transaction with the error RandomTeamSelector__NoSelectionOptionsAvailable.

 return selection.teamOptions;
}
Enter fullscreen mode Exit fullscreen mode

Finally, the function returns the teamOptionsarray from the selection, which contains the IDs of the available teams.

Let’s move on to the chooseTeamfunction, which allows a manager to finalize their team selection.

The Decision Maker: chooseTeam Function

In the strategic game of our smart contract, the chooseTeam function is like the final whistle that seals the fate of the match. It’s where the manager makes the definitive call, locking in their team choice.

Play by Play:

The Final Call: This function is the decisive moment where the manager selects their team from the available options.

Ensuring the Rules Are Followed: It verifies that the selection process is ongoing and that the chosen team is a valid option.

Locking in the Choice: Once the checks are passed, it finalizes the manager’s team selection, much like a coach submitting the final lineup.

Line by Line Code Breakdown:

function chooseTeam(uint256 teamId) public {
Enter fullscreen mode Exit fullscreen mode

This line declares the function chooseTeam, which is a public function that allows managers to choose a team. It takes one parameter, teamId, which is the ID of the team being chosen.

ManagerSelection storage selection = s_managerSelections[msg.sender];
Enter fullscreen mode Exit fullscreen mode

Here, the function retrieves the ManagerSelectionstruct for the manager calling the function (indicated by msg.sender) from the s_managerSelectionsmapping.

 require(
        selection.selectedTeam == SELECTION_ONGOING,
        "Selection process not ongoing or already completed."
    );
Enter fullscreen mode Exit fullscreen mode

This line checks that the selection process is currently ongoing for the manager. If not, it will revert the transaction with an error message.

bool validChoice = false;
    for (uint256 i = 0; i < selection.teamOptions.length; i++) {
        if (selection.teamOptions[i] == teamId) {
            validChoice = true;
            break;
        }
    }
Enter fullscreen mode Exit fullscreen mode

The function then iterates through the teamOptionsarray to check if the teamIdprovided is a valid option. If it finds a match, it sets validChoiceto true.

if (!validChoice) {
        revert RandomTeamSelector__InvalidTeamChoice();
    }
Enter fullscreen mode Exit fullscreen mode

If no valid option is found (validChoiceremains false), the function reverts the transaction with the error RandomTeamSelector__InvalidTeamChoice.

selection.selectedTeam = teamId;
    emit TeamChosen(msg.sender, teamId);
}
Enter fullscreen mode Exit fullscreen mode

Once a valid choice is confirmed, the function sets the selectedTeamto the chosen teamIdand emits the TeamChosenevent, indicating that the manager has made their selection.

Smart Contract Function Flow Summary

The RandomTeamSelector.sol smart contract orchestrates a series of functions to manage the process of random team selection. Here’s a summary of the flow:

Initialization: The contract begins with the requestRandomTeamNamesfunction, where the owner initiates the random selection process for a manager. This function checks if the manager has already made a selection and, if not, requests randomness from the VRF Coordinator.

Randomness Fulfillment: Upon receiving randomness from the VRF Coordinator, the fulfillRandomWordscallback function is triggered. It processes the random words to generate team options and updates the manager’s selection status to ongoing.

Team Selection: Managers can then call the teamNamefunction to retrieve the name of the team that has been selected for them, provided that the selection has been made.

Reviewing Options: Before making a selection, managers can review their available team options using the getTeamOptionsfunction, which returns an array of team IDs that are still in the running.

Finalizing Selection: Finally, the chooseTeamfunction allows managers to select their team from the provided options. It ensures the choice is valid and updates the manager’s selection, concluding the process.

Each function plays a critical role in ensuring the integrity and fairness of the random team selection process within the smart contract environment. The functions work together to manage the selection from initiation to finalization, providing transparency and control to the contract owner and participating managers.

And this is where I stand right now on the project. I know that I'll have to do some refactoring with some of these functions, but I'll likely leave that until after I write some tests and possibly after some interaction with a front-end.

The code lives here.

💖 💪 🙅 🚩
charlesj_dev
Charles Jones

Posted on June 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related