NEW

CCIP is now live for all developers. See what's new.

Back

VRF Subscription Balance Monitor

Automatically top-up your VRF subscription balances using Chainlink Automation to ensure there is sufficient funding for requests.

Overview

Automatically top-up your VRF subscription balances using Chainlink Automation to ensure there is sufficient funding for requests. View the code on GitHub.

Objective

This example shows how to automate the process for funding VRF subscription balances. You will deploy and test a VRF subscription balance manager contract that monitors multiple subscriptions and tops them up with LINK as necessary. You can set up this contract to monitor existing VRF subscriptions. Alternatively, you will create two VRF subscriptions for testing: one that is underfunded, and another that is adequately funded. When you test the subscription balance manager contract, it only tops up the underfunded subscription.

Before you begin

Before you start this tutorial, complete the following items:

  • If you are new to smart contract development, learn how to Deploy Your First Smart Contract.
  • Install and configure a cryptocurrency wallet like MetaMask.
  • Add testnet funds to your wallet:
    • Add the gas currency for the chain where your contract is deployed. For this example, use Sepolia. Get 0.5 Sepolia ETH.
    • Add ERC-677 LINK. You can get 20 Sepolia testnet LINK from faucets.chain.link.
  • Gather the subscription IDs for any existing VRF subscriptions you would like to monitor. If you do not have any VRF subscriptions, you will create two for demo purposes in this tutorial.

Steps to implement

This tutorial requires you to set up the following components in VRF before you use Automation to monitor VRF subscriptions:

If you already have existing VRF subscriptions you would like to monitor, skip to the next step.

1 Create VRF subscriptions to monitor

If you don't already have existing VRF subscriptions you would like to monitor, create two VRF subscriptions for demo purposes. One subscription will be adequately funded, and the other subscription will be underfunded intentionally so that the subscription balance monitor contract can fund it.

  1. Open the VRF Subscription Manager, and connect your wallet.

  2. Create two subscriptions:

    • Fund one subscription with 12 testnet LINK to serve as the adequately funded VRF subscription
    • Fund the other subscription with 1 testnet LINK to serve as the underfunded VRF subscription
2 Deploy VRF-compatible contracts as consumers

In this section, you will deploy the same VRF contract twice. Each deployed contract will serve as a consuming contract for one of your VRF subscriptions.

  1. Deploy this VRF-compatible contract for your VRF subscription:

    This VRFD20.sol contract uses VRF to randomly assign you a Game of Thrones house. The VRF coordinator address and key hash values are hardcoded in this contract for Sepolia. If you want to use a different testnet, you must update those values before deploying the contract.

  2. Under the Solidity compiler tab, compile the contract.

  3. Under the Deploy and run transactions tab, select Injected Provider - MetaMask for the Environment field. Make sure the VRFD20.sol contract is selected in the Contract field.

  4. Deploy the contract, passing in the subscription ID of your adequately funded subscription.

  5. After the contract is deployed successfully, copy the contract address from Remix. Navigate back to the VRF Subscription Manager and add the contract address as a consumer for your adequately funded subscription.

  6. Navigate back to Remix to deploy the contract once more. Change the subscription ID to the subscription ID of your intentionally underfunded subscription, and click Deploy.

  7. Once more, after the contract is deployed successfully, copy the second contract address from Remix. Navigate back to the VRF Subscription Manager and add this second contract address as a consumer for your intentionally underfunded subscription.

3 Deploy the subscription balance monitor contract

In this section, you will deploy the subscription balance monitor contract, using the same address that is the admin owner for both of your VRF subscriptions. The contract will monitor any VRF subscriptions owned by this address, and fund any underfunded subscriptions using funds owned by this address.

Get required inputs for the balance monitor contract

The constructor for this contract requires the following information for the supported network that you want to deploy on:

  • The LINK token address, which is found on the LINK token contracts page.
  • The Automation registry address, which is found on the Automation supported networks page.
  • The VRF coordinator address, which is found on the VRF Subscription Supported Networks page.
  • Minimum wait period, which you can use to add buffer time between funding multiple subscription IDs. For demo purposes, you will monitor only two VRF subscriptions, so you can set this value to 60 seconds.

For Sepolia, these values are all consolidated here:

ItemValue
LINK token address0x779877A7B0D9E8603169DdbD7836e478b4624789
VRF Coordinator0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
Automation registry address0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
  1. Open the VRFSubscriptionBalanceMonitor.sol contract in Remix.

  2. Under the Solidity compiler tab, compile the contract.

  3. Under the Deploy and run transactions tab, select Injected Provider - MetaMask for the Environment field.

  4. Expand the Deploy field and enter the values for Sepolia:

    ItemValue
    LINKTOKENADDRESS0x779877A7B0D9E8603169DdbD7836e478b4624789
    COORDINATORADDRESS0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
    KEEPERREGISTRYADDRESS0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
    MINWAITPERIODSECONDS60
  5. Click Deploy. MetaMask opens and prompts you to confirm the contract deployment transaction.

  6. Copy the address of your newly deployed contract. You can find this in Sepolia Etherscan through the contract deployment transaction, or in Remix in the Deployed Contracts section.

4 Register the subscription balance monitor contract on Automation

In this section, you will register an upkeep on Chainlink Automation to run the subscription balance monitor contract. This enables Automation to check whether you have any underfunded VRF subscriptions, and if so, top up their balances appropriately.

Register an upkeep

Registering an upkeep on Chainlink Automation creates a smart contract that will run your VRF subscription balance monitor contract.

  1. Click Register new Upkeep.

  2. Select the Custom logic trigger.

  3. Input the address of your deployed subscription balance monitor contract.

  4. Get the ABI from Remix.

  5. Enter the values for the constructor.

    ItemValue
    LINKTOKENADDRESS0x779877A7B0D9E8603169DdbD7836e478b4624789
    COORDINATORADDRESS0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
    KEEPERREGISTRYADDRESS0xE16Df59B887e3Caa439E0b29B42bA2e7976FD8b2
    MINWAITPERIODSECONDS60
  6. Fund the upkeep with enough LINK to monitor your VRF subscriptions.

Examine the code

This section explains how the VRFSubscriptionBalanceMonitor.sol contract tracks your VRF subscriptions and uses Chainlink Automation to fund your underfunded subscriptions. You can view the code for the full contract on GitHub.

Tracking subscriptions

The VRFSubscriptionBalanceMonitor.sol contract tracks your VRF subscriptions by creating a watchlist and storing attributes of each subscription required to monitor when they need funding.

The setWatchList() function creates the watchlist of subscriptions, validates it, and then sets a Target for each subscription. The Target struct helps track whether a subscription is active, its minimum balance, when it was last funded, and the amount of LINK (in juels) to send to the subscription each time it is topped up.

struct Target {
    bool isActive;
    uint96 minBalanceJuels;
    uint96 topUpAmountJuels;
    uint56 lastTopUpTimestamp;
  }

...

/**
   * @notice Sets the list of subscriptions to watch and their funding parameters.
   * @param subscriptionIds the list of subscription ids to watch
   * @param minBalancesJuels the minimum balances for each subscription
   * @param topUpAmountsJuels the amount to top up each subscription
   */
  function setWatchList(
    uint64[] calldata subscriptionIds,
    uint96[] calldata minBalancesJuels,
    uint96[] calldata topUpAmountsJuels
  ) external onlyOwner {
    if (subscriptionIds.length != minBalancesJuels.length || subscriptionIds.length != topUpAmountsJuels.length) {
      revert InvalidWatchList();
    }
    uint64[] memory oldWatchList = s_watchList;
    for (uint256 idx = 0; idx < oldWatchList.length; idx++) {
      s_targets[oldWatchList[idx]].isActive = false;
    }
    for (uint256 idx = 0; idx < subscriptionIds.length; idx++) {
      if (s_targets[subscriptionIds[idx]].isActive) {
        revert DuplicateSubcriptionId(subscriptionIds[idx]);
      }
      if (subscriptionIds[idx] == 0) {
        revert InvalidWatchList();
      }
      if (topUpAmountsJuels[idx] <= minBalancesJuels[idx]) {
        revert InvalidWatchList();
      }
      s_targets[subscriptionIds[idx]] = Target({
        isActive: true,
        minBalanceJuels: minBalancesJuels[idx],
        topUpAmountJuels: topUpAmountsJuels[idx],
        lastTopUpTimestamp: 0
      });
    }
    s_watchList = subscriptionIds;
  }

The getUnderfundedSubscriptions() function assesses each subscription in the watchlist and creates a list of subscriptions that need to be funded. Using the attributes stored in the Target struct for each subscription, this function adds a subscription to the needsFunding list if the following conditions are met:

  • The subscription is underfunded
  • The owning contract has a sufficient balance to fund the subscription
  • It's been long enough since the last time the subscription's balance was topped up
/**
 * @notice Gets a list of subscriptions that are underfunded.
 * @return list of subscriptions that are underfunded
 */
function getUnderfundedSubscriptions() public view returns (uint64[] memory) {
  uint64[] memory watchList = s_watchList;
  uint64[] memory needsFunding = new uint64[](watchList.length);
  uint256 count = 0;
  uint256 minWaitPeriod = s_minWaitPeriodSeconds;
  uint256 contractBalance = LINKTOKEN.balanceOf(address(this));
  Target memory target;
  for (uint256 idx = 0; idx < watchList.length; idx++) {
    target = s_targets[watchList[idx]];
    (uint96 subscriptionBalance, , , ) = COORDINATOR.getSubscription(watchList[idx]);
    if (
      target.lastTopUpTimestamp + minWaitPeriod <= block.timestamp &&
      contractBalance >= target.topUpAmountJuels &&
      subscriptionBalance < target.minBalanceJuels
    ) {
      needsFunding[count] = watchList[idx];
      count++;
      contractBalance -= target.topUpAmountJuels;
    }
  }
  if (count < watchList.length) {
    assembly {
      mstore(needsFunding, count)
    }
  }
  return needsFunding;
}

Using Automation

The VRFSubscriptionBalanceMonitor.sol contract uses Chainlink Automation to top up underfunded subscriptions:

  • In checkUpkeep(), it pulls the list of underfunded subscriptions. If this list is empty, it indicates that the upkeep is not needed, and Automation takes no further action.
  • Automation only runs performUpkeep() if there are underfunded subscriptions, and it tops up any subscriptions that need funding.
/**
 * @notice Gets list of subscription ids that are underfunded and returns a keeper-compatible payload.
 * @return upkeepNeeded signals if upkeep is needed, performData is an abi encoded list of subscription ids that need funds
 */
function checkUpkeep(
  bytes calldata
) external view override whenNotPaused returns (bool upkeepNeeded, bytes memory performData) {
  uint64[] memory needsFunding = getUnderfundedSubscriptions();
  upkeepNeeded = needsFunding.length > 0;
  performData = abi.encode(needsFunding);
  return (upkeepNeeded, performData);
}

/**
 * @notice Called by the keeper to send funds to underfunded addresses.
 * @param performData the abi encoded list of addresses to fund
 */
function performUpkeep(bytes calldata performData) external override onlyKeeperRegistry whenNotPaused {
  uint64[] memory needsFunding = abi.decode(performData, (uint64[]));
  topUp(needsFunding);
}

Stay updated on the latest Chainlink news