In this lesson, you will learn about two services that Chainlink provides for smart contract developers: Chainlink VRF and Chainlink Automations. These services enable smart contracts to access verifiable randomness and decentralized automation, which can enhance the functionality and security of your dApps.
Chainlink VRF is a service that provides verifiable and unpredictable random numbers for smart contracts. It uses a cryptographic function that takes a secret key and a seed as inputs, and produces a random number and a proof as outputs. The proof can be verified by anyone using the public key, but the random number cannot be predicted or manipulated by anyone, not even the oracle operators or miners. This ensures that the randomness is fair and secure for applications that need it, such as gaming, NFTs, or lotteries.
You can learn more about Chainlink VRF from the official documentation.
The Chainlink VRF works by allowing a smart contract to request a random number from a Chainlink node. The node operator running the Chainlink node will then use the VRF to generate a random number and provide it to the smart contract. The process involves the following steps:
Chainlink VRF offers several benefits for smart contract developers and users who need verifiable randomness:
To use Chainlink VRF inside a smart contract we first need to fund the LINK tokens needed to carry out this processing. There are two methods for requesting randomness with Chainlink VRF v2:
Direct funding method doesn't require a subscription and is optimal for one-off requests for randomness. This method also works best for applications where your end-users must pay the fees for VRF because the cost of the request is determined at request time.
Unlike the subscription method, the direct funding method does not require you to create subscriptions and pre-fund them. Instead, you must directly fund consuming contracts with LINK tokens before they request randomness. Because the consuming contract directly pays the LINK for the request, the cost is calculated during the request and not during the callback when the randomness is fulfilled. If you need help estimating the costs, check this documentation page.
To set up your consuming contract, you must take care of these steps:
Your contract must inherit VRFV2WrapperConsumerBase.
Your contract must implement the fulfillRandomWords
function, which is the callback VRF function. Here, you add logic to handle the random values after they are returned to your contract.
Submit your VRF request by calling the requestRandomness
function in the VRFV2WrapperConsumerBase contract. Include the following parameters in your request:
requestConfirmations
: The number of block confirmations the VRF service will wait to respond. The minimum and maximum confirmations for your network can be found here.callbackGasLimit
: The maximum amount of gas to pay for completing the callback VRF function.numWords
: The number of random numbers to request. You can find the maximum number of random values per request for your network in the Supported networks page.You can learn more about how Chainlink process the request in this documentation page.
To test this process we're using this smart contract:
// SPDX-License-Identifier: MIT
// An example of a consumer contract that directly pays for each request.
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";
import "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol";
/**
* Request testnet LINK and ETH here: https://faucets.chain.link/
* Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
*/
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract VRFv2DirectFundingConsumer is
VRFV2WrapperConsumerBase,
ConfirmedOwner
{
event RequestSent(uint256 requestId, uint32 numWords);
event RequestFulfilled(
uint256 requestId,
uint256[] randomWords,
uint256 payment
);
struct RequestStatus {
uint256 paid; // amount paid in link
bool fulfilled; // whether the request has been successfully fulfilled
uint256[] randomWords;
}
mapping(uint256 => RequestStatus)
public s_requests; /* requestId --> requestStatus */
// past requests Id.
uint256[] public requestIds;
uint256 public lastRequestId;
// Depends on the number of requested values that you want sent to the
// fulfillRandomWords() function. Test and adjust
// this limit based on the network that you select, the size of the request,
// and the processing of the callback request in the fulfillRandomWords()
// function.
uint32 callbackGasLimit = 100000;
// The default is 3, but you can set this higher.
uint16 requestConfirmations = 3;
// For this example, retrieve 2 random values in one request.
// Cannot exceed VRFV2Wrapper.getConfig().maxNumWords.
uint32 numWords = 2;
// Address LINK - hardcoded for Sepolia
address linkAddress = 0x779877A7B0D9E8603169DdbD7836e478b4624789;
// address WRAPPER - hardcoded for Sepolia
address wrapperAddress = 0xab18414CD93297B0d12ac29E63Ca20f515b3DB46;
constructor()
ConfirmedOwner(msg.sender)
VRFV2WrapperConsumerBase(linkAddress, wrapperAddress)
{}
function requestRandomWords()
external
onlyOwner
returns (uint256 requestId)
{
requestId = requestRandomness(
callbackGasLimit,
requestConfirmations,
numWords
);
s_requests[requestId] = RequestStatus({
paid: VRF_V2_WRAPPER.calculateRequestPrice(callbackGasLimit),
randomWords: new uint256[](0),
fulfilled: false
});
requestIds.push(requestId);
lastRequestId = requestId;
emit RequestSent(requestId, numWords);
return requestId;
}
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
require(s_requests[_requestId].paid > 0, "request not found");
s_requests[_requestId].fulfilled = true;
s_requests[_requestId].randomWords = _randomWords;
emit RequestFulfilled(
_requestId,
_randomWords,
s_requests[_requestId].paid
);
}
function getRequestStatus(
uint256 _requestId
)
external
view
returns (uint256 paid, bool fulfilled, uint256[] memory randomWords)
{
require(s_requests[_requestId].paid > 0, "request not found");
RequestStatus memory request = s_requests[_requestId];
return (request.paid, request.fulfilled, request.randomWords);
}
/**
* Allow withdraw of Link tokens from the contract
*/
function withdrawLink() public onlyOwner {
LinkTokenInterface link = LinkTokenInterface(linkAddress);
require(
link.transfer(msg.sender, link.balanceOf(address(this))),
"Unable to transfer"
);
}
}
For this contract to work, we're going to make sure that the consuming contract has enough LINK. To do so, complete these two steps:
1: Acquire testnet LINK;
2: Fund your contract with testnet LINK. For this example, funding your contract with 2 LINK should be sufficient.
Now that the contract is funded, we're going to test the randomness function with these steps:
Call the requestRandomWords()
function to send the request for random values to Chainlink VRF;
To fetch the request ID of your request, call lastRequestId()
;
After the oracle returns the random values to your contract, the mapping s_requests is updated. The received random values are stored in s_requests[_requestId].randomWords
.
Call getRequestStatus()
and specify the requestId to display the random words.
To complete these steps using Remix, you can follow this tutorial.
(1) Introduction to Chainlink VRF | Chainlink Documentation: https://docs.chain.link/vrf/v2/introduction/
(2) What is Chainlink VRF, and how does it work? - Cointelegraph: https://cointelegraph.com/news/what-is-chainlink-vrf-and-how-does-it-work
(3) How Chainlink VRF works - Smart Contract Research Forum: https://www.smartcontractresearch.org/t/how-chainlink-vrf-works/2896
(4) Security Considerations: https://docs.chain.link/vrf/v2/security
Chainlink Automations, formerly known as Keepers, is a service that enables smart contract developers to automate their contract functions in a decentralized manner. It uses a network of Chainlink nodes that monitor the contract state and trigger transactions when certain conditions are met. For example, a smart contract could use Chainlink Automations to automatically rebalance its portfolio, distribute rewards, or execute trades. Chainlink Automations saves developers time and resources, and provides reliable and scalable automation for their dApps.
You can learn more about Chainlink Automations from the official website.
Chainlink Automations works by allowing a smart contract to register an upkeep with the Chainlink Automation Registry contract. An upkeep is a job or task that the smart contract wants to execute periodically or conditionally. The upkeep can be based on time (e.g., every hour) or custom logic (e.g., when the price of an asset reaches a certain level). The smart contract also needs to provide some payment in LINK tokens to fund the upkeep.
The Chainlink Automation Registry assigns the upkeep to one or more Chainlink nodes that have registered as Automation Nodes. These nodes are the same ones that provide data feeds and verifiable randomness for other Chainlink services. The nodes use a turn-taking algorithm to service the upkeeps in a fair and random manner.
The assigned Chainlink nodes monitor the smart contract state and evaluate the upkeep logic off-chain, using a local simulation of the blockchain. This reduces the gas costs and latency of the automation process. When the nodes detect that the upkeep condition is met, they initiate an on-chain transaction to execute the smart contract function.
The smart contract verifies that the transaction is coming from an authorized Chainlink node and that the upkeep logic is satisfied. If the verification passes, the smart contract accepts the transaction and performs the function. If the verification fails, the smart contract rejects the transaction and can request a new one.
Chainlink Automations offers several benefits for smart contract developers and users who need decentralized automation:
There are two possible ways that Chainlink Automation will check when to execute smart contract functions:
Time-based trigger: Use a time-based trigger to execute your function according to a time schedule. This feature is also called the Job Scheduler and it is a throwback to the Ethereum Alarm Clock. Time-based trigger contracts do not need to be compatible with the AutomationCompatibleInterface
contract.
Custom logic trigger: Use a custom logic trigger to provide custom solidity logic that Automation Nodes evaluate (off-chain) to determine when to execute your function on-chain. Your contract must meet the requirements to be compatible with the AutomationCompatibleInterface
contract. Custom logic examples include checking the balance on a contract, only executing limit orders when their levels are met, any one of our coded examples, and many more.
To use Chainlink Automation, contracts must meet the following requirements:
AutomationCompatible.sol
;AutomationCompatibleInterface
from the library to ensure your checkUpkeep
and performUpkeep
function definitions match the definitions expected by the Chainlink Automation Network.checkUpkeep
function that contains the logic that will be executed off-chain to see if performUpkeep
should be executed. checkUpkeep
can use on-chain data and a specified checkData
parameter to perform complex calculations off-chain and then send the result to performUpkeep
as performData
;performUpkeep
function that will be executed on-chain when checkUpkeep
returns true.After you deploy your contract, you'll need to setup the automation process in the Automation UI.
To test this process we're using this smart contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
// AutomationCompatible.sol imports the functions from both ./AutomationBase.sol and
// ./interfaces/AutomationCompatibleInterface.sol
import "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract Counter is AutomationCompatibleInterface {
/**
* Public counter variable
*/
uint public counter;
/**
* Use an interval in seconds and a timestamp to slow execution of Upkeep
*/
uint public immutable interval;
uint public lastTimeStamp;
constructor(uint updateInterval) {
interval = updateInterval;
lastTimeStamp = block.timestamp;
counter = 0;
}
function checkUpkeep(
bytes calldata /* checkData */
)
external
view
override
returns (bool upkeepNeeded, bytes memory /* performData */)
{
upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
// We don't use the checkData in this example. The checkData is defined when the Upkeep was registered.
}
function performUpkeep(bytes calldata /* performData */) external override {
//We highly recommend revalidating the upkeep in the performUpkeep function
if ((block.timestamp - lastTimeStamp) > interval) {
lastTimeStamp = block.timestamp;
counter = counter + 1;
}
// We don't use the performData in this example. The performData is generated by the Automation Node's call to your checkUpkeep function
}
}
For this contract to work, we're going to register it in the Automation UI
After you set up this Upkeep, you can view the Upkeep details at the Automation UI and also by calling the smart contract functions:
counter
to view the amount of times that the function was called;lastTimeStamp
to view the timestamp of the last time that the contract was updated by the automation.To complete these steps using Remix, you can follow this tutorial
(1) Introduction to Chainlink Automation | Chainlink Documentation: https://docs.chain.link/chainlink-automation/introduction/
(2) Chainlink Automation Architecture | Chainlink Documentation: https://docs.chain.link/chainlink-automation/overview/
(3) Reliable, high-performance smart contract automation: https://chain.link/automation
(4) Chainlink Automation Best Practices: https://docs.chain.link/chainlink-automation/compatible-contract-best-practice