Building Randomness with Chainlink VRF: Part 2
Charles Jones
Posted on June 16, 2024
“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 requestRandomTeamNames
to 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 requestRandomTeamNames
function 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 SelectionMade
event.
Let’s break down each line of the requestRandomTeamNames function for clarity:
function requestRandomTeamNames(
address manager
) public onlyOwner returns (uint256 requestId) {
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 onlyOwner
modifier, 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();
}
Here, we’re checking if the manager has already selected a team. If the teamOptions
array 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(
This line assigns a new requestId
by calling the requestRandomWords
function 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 requestRandomWords
function 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})
)
})
);
This section is constructing a RandomWordsRequest
struct 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;
This line maps the requestId
to the manager’s address, storing the association in a state variable.
s_managerSelections[manager].selectedTeam = SELECTION_ONGOING;
Here, the function sets the selectedTeam
status to SELECTION_ONGOING
for the manager, indicating that the selection process is currently underway.
emit SelectionMade(requestId, manager);
}
Finally, the function emits an event SelectionMade
with the requestId
and 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 fulfillRandomWords
function 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 requestId
and 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 requestId
and prepares an array teamOptions
to 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 selectedTeam
status is set to SELECTION_ONGOING, indicating the play is in progress.
Announcing the Outcome: Finally, the SelectionRevealed
event 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 {
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];
This line retrieves the manager’s address that is associated with the given requestId
from a mapping called s_requestToManager
.
uint256[] memory teamOptions = new uint256;
Here, a new temporary array called teamOptions
is 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;
}
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 teamOptions
array.
s_managerSelections[manager] = ManagerSelection({
teamOptions: teamOptions,
selectedTeam: SELECTION_ONGOING
});
The s_managerSelections
mapping is updated for the manager with a new ManagerSelection
struct, which includes the teamOptions
array and sets the selectedTeam
status to SELECTION_ONGOING
.
emit SelectionRevealed(requestId, teamOptions);
}
Finally, the SelectionRevealed
event is emitted, which includes the requestId
and 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 teamName
function 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) {
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];
Here, the function retrieves the ManagerSelection
struct from the s_managerSelections
mapping for the given player address and stores it in a local variable selection.
if (selection.selectedTeam == 0) {
revert RandomTeamSelector__SelectionNotMade();
}
This line checks if the selectedTeam
property 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);
}
Finally, the function returns the name of the selected team by calling another function getTeamName
and passing the selectedTeam
ID 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 getTeamOptions
function 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) {
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];
Here, the function retrieves the ManagerSelection
struct associated with the manager’s address from the s_managerSelections
mapping.
if (selection.selectedTeam != SELECTION_ONGOING) {
revert RandomTeamSelector__NoSelectionOptionsAvailable();
}
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;
}
Finally, the function returns the teamOptions
array from the selection, which contains the IDs of the available teams.
Let’s move on to the chooseTeam
function, 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 {
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];
Here, the function retrieves the ManagerSelection
struct for the manager calling the function (indicated by msg.sender
) from the s_managerSelections
mapping.
require(
selection.selectedTeam == SELECTION_ONGOING,
"Selection process not ongoing or already completed."
);
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;
}
}
The function then iterates through the teamOptions
array to check if the teamId
provided is a valid option. If it finds a match, it sets validChoice
to true.
if (!validChoice) {
revert RandomTeamSelector__InvalidTeamChoice();
}
If no valid option is found (validChoice
remains false), the function reverts the transaction with the error RandomTeamSelector__InvalidTeamChoice
.
selection.selectedTeam = teamId;
emit TeamChosen(msg.sender, teamId);
}
Once a valid choice is confirmed, the function sets the selectedTeam
to the chosen teamId
and emits the TeamChosen
event, 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 requestRandomTeamNames
function, 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 fulfillRandomWords
callback 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 teamName
function 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 getTeamOptions
function, which returns an array of team IDs that are still in the running.
Finalizing Selection: Finally, the chooseTeam
function 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.
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
November 29, 2024