Overview
The lootbox utility contract uses Chainlink VRF to determine a random reward that you can distribute as a Loot Box to users. This allows you to issue rewards fairly and transparently using verified randomness that the recipients can verify onchain.
Objective
This tutorial demonstrates how to create a reward distribution dApp to distribute in-game rewards to players. In this tutorial, you will use example code from the smartcontractkit/quickstarts-lootbox repository. You will configure the example to work on a testnet of your choice, use an RPC endpoint of your choice, and distribute in-game rewards to players by using a lootbox contract. This example uses the HardHat framework to help you complete the testing and deployment steps.
This example contract supports distributing any rewards that adhere to fungible, non-fungible, and other configuration standards. For example, you can create a set of in-game rewards, transfer them to the lootbox contract when you deploy it, and then the lootbox contract manages in-game reward distributions.
Before you begin
Before you start this tutorial, complete the following items:
- Install git
- Run
git --versionto check the installation. You should see an output similar togit version x.x.x.
- Run
- Install Nodejs 16.0.0 or higher
- Run
node --versionto check the installation. You should see an output similar tov16.x.x. - Optionally, you can also Install Yarn and use it to run this example instead of using
npm.
- Run
- Create an Etherscan API key if you do not already have one.
- Create an account with Infura or Alchemy to use as an RPC endpoint if you do not already have one. Alternatively, you can use your own testnet clients.
Your deployer account is the main account that you will use to deploy the lootbox contract, distribute rewards, pay for transaction fees, and pay for VRF costs. This is also the owner account.
-
Ensure the deployer account owns all the assets you wish to distribute as rewards.
-
To deploy this contract on testnets, ensure the deployer account has testnet LINK and testnet ETH (Sepolia). Use the LINK faucet to retrieve 20 testnet LINK and 0.1 Sepolia ETH.
-
Ensure the deployer account has enough LINK to fund the VRF subscription. If you don't already have a VRF subscription, the deployment script will create and fund one for you.
- Estimate the minimum subscription balance that VRF requires to process your randomness request. The minimum subscription balance provides a buffer against gas volatility, and only the actual cost of your request will be deducted from your account. If your subscription is underfunded, your VRF request will be pending for 24 hours. If that happens, check the Subscription Manager to see the additional balance needed.
- The initial funding amount is set to 10 LINK. Optionally, you can modify the initial funding amount in
network-config.ts.
-
Create a second account in your MetaMask wallet. This account is called the receiving account in this tutorial, and its address is called the opener address. You will use this receiving account to open a test lootbox and receive the rewards as a user. Configure the account to display the rewards that you want to distribute from the lootbox.
Steps to implement
1 Clone the example repo and install dependencies
Clone the repo and install all dependencies.
If you want to use npm, run:
git clone git@github.com:smartcontractkit/quickstarts-lootbox.git && \
cd quickstarts-lootbox && \
npm install
Alternatively, you can use yarn to install dependencies:
git clone git@github.com:smartcontractkit/quickstarts-lootbox.git && \
cd quickstarts-lootbox && \
yarn install
2 Configure your project
Copy the .env.example file to .env and fill in the values.
cp .env.example .env
3 Configure the Hardhat project
Hardhat is an Ethereum development environment that is used here to configure and deploy the lootbox contract. You need the following information:
- The RPC URL for your deployment network, using an RPC node provider such as Infura or Alchemy.
- The private key for your deployer account. If your deployer account is in MetaMask, export your private key from MetaMask.
- An Etherscan API key for contract verification. Create an Etherscan API key if you do not already have one.
-
In your
.envfile, fill in these values:Parameter Description Example NETWORK_RPC_URLThe RPC URL for the network you want to deploy to. https://sepolia.infura.io/v3/your-api-keyPRIVATE_KEYThe private key of the account you want to deploy from. 0xabc123abc123abc123abc123abc123...ETHERSCAN_API_KEYThe API key for Etherscan needed for contract verification. ABC123ABC123ABC123ABC123ABC123ABC1 -
If you're using a deployment network other than Sepolia or Ethereum: configure your deployment network in both the
.envfile and in thehardhat.config.tsfile.
4 Set your contract parameters
In your .env file, fill in these values:
| Parameter | Description | Example |
|---|---|---|
LOOTBOX_FEE_PER_OPEN | The fee per open in ETH | 0.05 |
LOOTBOX_AMOUNT_DISTRIBUTED_PER_OPEN | The amount of reward units distributed per open | 1 |
LOOTBOX_OPEN_START_TIMESTAMP | The start timestamp in UNIX time for the public open. Leave blank to start immediately. | 1630000000 |
VRF_SUBSCRIPTION_ID | A funded Chainlink VRF subscription ID. If you leave this blank, a new subscription will be created and funded on deploy. | 7123 |
The LOOTBOX_FEE_PER_OPEN allows you to pass transaction and VRF costs to the user when they open the lootbox.
5 Define the rewards
You can distribute multiple types of in-game rewards using this lootbox contract. The lootbox contract supports any rewards of the following token standards:
The amounts per unit are used to calculate the lootbox supply of rewards. For example, if you want to distribute 100 of a specific NFT, you would set the totalAmount to 100000000000000000000 and the amountPerUnit to 1000000000000000000. This would result in a lootbox supply of 100 units. For testing, you can distribute smaller amounts.
Add each type of in-game reward you want to distribute by following the format below.
There's an example configuration file in scripts/data/tokens.json which includes a list of all supported token types:
{
"tokenType": "ERC20",
"assetContract": "0x0000000000000000000000000000000000000001",
"totalAmount": "100000000000000000000",
"amountPerUnit": "10000000"
}
{
"tokenType": "ERC721",
"assetContract": "0x0000000000000000000000000000000000000002",
"tokenIds": ["1", "2"]
}
{
"tokenType": "ERC1155",
"assetContract": "0x0000000000000000000000000000000000000003",
"tokenId": "0",
"totalAmount": "100",
"amountPerUnit": "10"
}
6 Create an allowlist for private mints
The Merkle tree for the private opening mode is generated from the address list in the scripts/data/whitelist.json file. Edit the file and add all the addresses you want to list.
For this tutorial, add the address for your secondary account that you'll use to receive the rewards when you test opening the lootbox.
If you don't want to do a private mint, leave the whitelist.json file empty, and the contract will be initialized in public opening mode.
After deployment, you can optionally change some contract parameters by calling the following functions from the owner account:
| Function | Description | Parameters |
|---|---|---|
setWhitelistRoot | Set new Merkle root for the allow list. | whitelistRoot |
setPrivateOpen | Enable/disable public opening mode. | privateOpenEnabled |
7 Deploy and run the example lootbox contract
Now that your example is configured, you can test, deploy, and run the example. Then, switch accounts to act as a user who received the lootbox and receive the example rewards.
Run the npx hardhat run command and replace <network> with the network that you want to deploy to. The network must be configured in hardhat.config.ts.
npx hardhat run scripts/deploy.ts --network <network>
The deploy script also completes the following actions:
-
Approve each type of reward configured in
scripts/data/tokens.jsonfor the deployed contract address.This is a mandatory step because the contract must store the assets in its own balance. It is important to ensure the deployer account owns all the assets you want to distribute as rewards.
-
Generate a Merkle tree for the allowlist.
-
Create and fund a VRF subscription if one is not provided.
-
Add the deployed contract address as a consumer to the VRF subscription.
If you provided a subscription ID, make sure the deployer account is the owner of the subscription. Otherwise, comment out the
addVrfConsumerfunction in the deploy script and add the contract address manually. -
Verify the contract on Etherscan. This is important to show users the source code for the contract so they can confirm how it works. If you want to skip this step during testing, comment out the
verifyfunction in the deploy script.
Next, switch accounts and open the lootbox as a user to test the contract.
8 Open the lootbox and claim rewards
Once the contract is deployed and the start timestamp is reached, users can start opening the lootbox. The amount of rewards users receive depends on the amount of units they open by specifying the amountToOpen parameter and paying the corresponding fee.
-
Switch to your receiving account to act as a test user and copy its address for the next steps.
-
Open the lootbox. In Sepolia Etherscan:
- Call the
publicOpen()function if you initialized the contract with an empty allowlist. - Call the
privateOpen()function if you set up an allowlist. Generate the Merkle proof locally using merkletreejs, and pass in the opener address:
merkleTree.getHexProof(keccak256(allowlistedUserAddress))Pass the Merkle proof as input to
privateOpen()in Sepolia Etherscan. - Call the
-
The rewards are ready to claim when the randomness result from VRF is ready. To check this, navigate to the Read tab, call the
canClaimRewardsfunction and pass in the opener address. -
When the rewards are ready, navigate to the Write tab, call the
claimRewardsfunction to claim the reward and pass in the opener address.
Reference
Access control
You can control access to the lootbox with two modes: private and public. Use these modes to determine which users can open the lootbox. The owner account can update this setting after deployment, so you can test the opening functionality privately before making the lootbox available to the public.
Private mode
In this mode, the lootbox is only open to addresses on the allowlist by calling the privateOpen function and providing Merkle proof for the address. You can generate the Merkle proof locally using merkletreejs:
merkleTree.getHexProof(keccak256(whitelistedUserAddress))
The allowlist is set on contract deployment and can be changed by calling the setWhitelistRoot function from the owner account. The private mode can be enabled or disabled at any time by calling the setPrivateOpen function from the owner account.
Public mode
When the private mode is disabled, anyone can open the lootbox by calling the publicOpen function. It will do the same thing as the privateOpen function but without the Merkle proof.
Randomness
The contract uses Chainlink VRF to generate randomness which is used to determine the rewards the user receives.
Because the randomness is generated offchain, the contract will not be able to transfer the rewards immediately. Instead, it will store the request and once the randomness is received, the user or anyone else can call the claimRewards function to transfer the rewards to the user.
The lootbox creator can also call the claimRewards function and improve the user experience by transferring the rewards to the user immediately. You can automate this further by using Chainlink Automation.
Claim rewards
The rewards for an open request can be claimed by calling the claimRewards function and passing the opener address as the parameter. This will transfer the rewards to the opener address.
The claim function can only be called after the randomness is fulfilled. To check this, call the canClaimRewards function and pass the opener address as the parameter.
One address can only have one open request at a time. If you try to open the lootbox again before the previous request is fulfilled, the transaction reverts with a PendingOpenRequest error.
Withdraw funds
At any time, the owner can withdraw funds from the collected fees by calling the withdraw function. By doing so, the contract balance will be transferred to the owner account.
Code walkthrough
The main part of this tutorial is in the Lootbox.sol contract. The Lootbox contract manages the following tasks:
- Requests a random value from VRF for each open request
- Uses the random value as a seed to determine the reward
- Tracks and validates requests to open the lootbox
- Transfers rewards to the user, for valid requests
- Tracks how many rewards remain in the lootbox
- Validates user addresses against the allowlist, if in private mode
This section focuses on how VRF is used in the lootbox contract.
How the lootbox interacts with the VRF service
The processing done in the fulfillRandomWords callback function is minimal. Here, the function simply stores the randomness result from VRF and emits an event to indicate that the randomness is ready, so the lootbox open request can now be fulfilled. Reducing the computational processing in your fulfillRandomWords callback function makes the callback more gas efficient, and it reduces the chance that your callbackGasLimit will be too low for the VRF request to go through.
...
/// @notice Requests randomness from Chainlink VRF
/// @dev The VRF subscription must be active and sufficient LINK must be available
/// @return requestId The ID of the request
function _requestRandomness() internal returns (uint256 requestId) {
requestId = VRFCoordinatorV2Interface(i_vrfCoordinatorV2)
.requestRandomWords(
i_vrfKeyHash,
i_vrfSubscriptionId,
REQUEST_CONFIRMATIONS,
CALLBACK_GASLIMIT,
NUMWORDS
);
}
/// @inheritdoc VRFConsumerBaseV2
function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
) internal override {
s_requests[requestId].randomness = randomWords[0];
emit OpenRequestFulfilled(requestId, randomWords[0]);
}