NEW

Introducing Transporter, a new bridging app powered by CCIP. Go cross-chain.

Back

VRF-Enabled LootBox/Pack Contract

Build a pack contract using VRF.

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 --version to check the installation. You should see an output similar to git version x.x.x.
  • Install Nodejs 16.0.0 or higher
    • Run node --version to check the installation. You should see an output similar to v16.x.x.
    • Optionally, you can also Install Yarn and use it to run this example instead of using npm.
  • 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 [email protected]:smartcontractkit/quickstarts-lootbox.git && \
cd quickstarts-lootbox && \
npm install

Alternatively, you can use yarn to install dependencies:

git clone [email protected]: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:

  1. In your .env file, fill in these values:

    ParameterDescriptionExample
    NETWORK_RPC_URLThe RPC URL for the network you want to deploy to.https://sepolia.infura.io/v3/your-api-key
    PRIVATE_KEYThe private key of the account you want to deploy from.0xabc123abc123abc123abc123abc123...
    ETHERSCAN_API_KEYThe API key for Etherscan needed for contract verification.ABC123ABC123ABC123ABC123ABC123ABC1
  2. If you're using a deployment network other than Sepolia or Ethereum: configure your deployment network in both the .env file and in the hardhat.config.ts file.

4 Set your contract parameters

In your .env file, fill in these values:

ParameterDescriptionExample
LOOTBOX_FEE_PER_OPENThe fee per open in ETH0.05
LOOTBOX_AMOUNT_DISTRIBUTED_PER_OPENThe amount of reward units distributed per open1
LOOTBOX_OPEN_START_TIMESTAMPThe start timestamp in UNIX time for the public open. Leave blank to start immediately.1630000000
VRF_SUBSCRIPTION_IDA 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"
}
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:

FunctionDescriptionParameters
setWhitelistRootSet new Merkle root for the allow list.whitelistRoot
setPrivateOpenEnable/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:

  1. Approve each type of reward configured in scripts/data/tokens.json for 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.

  2. Generate a Merkle tree for the allowlist.

  3. Create and fund a VRF subscription if one is not provided.

  4. 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 addVrfConsumer function in the deploy script and add the contract address manually.

  5. 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 verify function 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.

  1. Switch to your receiving account to act as a test user and copy its address for the next steps.

  2. 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.

  3. The rewards are ready to claim when the randomness result from VRF is ready. To check this, navigate to the Read tab, call the canClaimRewards function and pass in the opener address.

  4. When the rewards are ready, navigate to the Write tab, call the claimRewards function 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]);
}

Stay updated on the latest Chainlink news