We continue our series of instructive articles with some special recommendations for both developers and auditors using Chainlink VRF integration.
In the beginning, we would like to express our heartfelt gratitude to the Chainlink VRF designers, community, everyone who supports it, the authors of all resource materials, and, of course, our in-team auditors who have assisted us by providing much-needed information and breaking the veil of secrecy!
Looking at this month’s never-ending hacks, one may wonder why they happen so frequently…
Have audit firms actually gotten worse at what they do???
This, in our opinion, is not the case; yet, the topic is rather tricky because, in certain ways, you can reduce the risks to yourself and your project! By the way, we are working on such a solution within the team and hope to deliver it shortly:
We believe there is no one who doubts that the basis of any secure integration is a special approach to writing code. Consequently, this article will be focused only on those aspects that can be really useful for making your code safe and secure from the start.
Therefore, below you will see not a typical article but a systematization of knowledge (SoK), in which I will rely on authors that I myself trust in this matter and, of course, our pessimistic.io auditors.
This is what we’ll cover in this post about using Chainlink VRF in your project! In this article, we also plan to go over the background of the most recent Chainlink VRF V2 and compare it to its earlier V1 iteration.
You will also find a list of tools and research for self-study, and we strongly recommend that you read it separately for better understanding! By the way, here are some vacant slots so if your project needs an audit — feel free to write to us, visit our public reports page here!
Let’s get in touch: gm@pessimistic.io!
Chainlink is a decentralized oracle network that links external data sources, APIs, and payment systems with blockchain-based smart contracts. In short, it makes it possible for smart contracts to interact with off-chain resources, paving the way for the creation of a new wave of decentralized applications (dApps).
A group of blockchain and cryptography experts, including Sergey Nazarov (Sergey Nazarov) andSteve Ellis, first unveiled their new project —ChainLink— in 2017. The pace of change during that time period can be roughly estimated by looking at the industry’s past records:
To address the shortcomings of current blockchain networks and to facilitate the creation of more sophisticated and complex dApps, the team created Chainlink.
Wikipedia defines an oracle as a person or thing that provides wise and insightful counsel or prophetic predictions. The word is believed to be related to Greek culture, referring to divine revelations given by priests or equivalent persons as a response to an inquiry.
A blockchain oracle is defined as an* external data agent that observes real-world events and reports them back to the blockchain to be used by smart contracts. *The use of decentralized data oracles is one of Chainlink’s distinguishing characteristics!
This topic is quite tricky because blockchain-based oracles are all very different — and are used in Web3 industry for completely different purposes, for example, to comply with sanctions screening or to put up-to-date information on the price of a currency pair.
But, in general, blockchain oracle is presented as a comprehensive system that gathers off-chain data, verifies it, and sends it to blockchain smart contracts.
Just look at this fantastic scheme:
Chainlink relies on a network of decentralized oracles that are managed by independent nodes, making the oracles trustworthy, secure, and impervious to manipulation.
Support for a wide range of data sources, APIs, and payment systems is another important aspect of Chainlink. Smart contracts can access data from a variety of sources using Chainlink, including social media platforms, weather data, and financial markets.
Below I’ve put together some great resources to help you get a good idea of what role Chainlink plays in the ecosystem! I encourage you to return to them in your spare time after reading this article:
Additionally, Chainlink enables smart contracts to communicate with external APIs and payment systems, facilitating the creation of sophisticated and cutting-edge dApps!
A Tiny Tip: You can also find even more awesome ChainLink implementations & resources in ChainLinkGod’s Twitter feed!
Whether your contract requires sports results, the latest weather, or any other publicly available data, the Chainlink contract library provides the tools required for our contract to consume it. You can even connect your Tesla car to it!
In order for you to form a proper idea of the subject of our conversation today — Entropy, let’s study what it is!
First things first, you should always keep in mind the following:
Entropy, in cyber security, is a measure of the randomness or diversity of a data-generating function. Data with full entropy is completely random and no patterns can be found. Low entropy data provides the ability or possibility to predict forthcoming generated values.
Knowing that, we can now understand the paradigm of Chainlink thinking in which they are presented as a provider of both super-accurate data with zero entropy (through their oracles) and data with high entropy through Chainlink VRF.
It is important to say that natural entropy can also be a reliable source of protection:
Although this topic is unrelated to the one we will be discussing today (Chainlink), it is nonetheless crucial to understanding what we will be discussing next…
A proper randomness is especially important in blockchain which heavily relies on entropy. For a better understanding of how randomness impacts both security and blockchain, create a “vanity” address — no matter what network you choose: either Bitcoin or EVM-compatible blockchain.
You can use Profanity2 on Ethereum (it is safe) for this purpose, but don’t forget about the story happened to Profanity1. Check out my article about the Profanity vulnerability.
If the previous example was difficult to grasp, no one (not even the most sleepy and inattentive reader 🙂) will be able to grasp the following one. I strongly recommend you investigating this topic further on your own in the future, as it is too broad for one article but crucial to comprehend.
Check out the CatPaper & the Catropy docs:
But let’s get back to our main topic for a while… I guess it goes without saying that if something that’s meant to be random isn’t truly random, then it’s gameable, isn’t it?
This might seem trivial but it is especially important for tasks that rely on unpredictable outcomes such as, for example, consensus mechanisms!
Many blockchain gaming and non-fungible token (NFT) DApps also require a tamper-proof and verifiable random number generation source to enable advanced features such as airdrop, lottery or gambling development.
What does this mean to us, though, in reality?
Well, if the seed is pseudo-random, that means it is 100% deterministic which means that rare NFT collection art-pieces can be established ahead of time (before their creation) and opportunistically minted!
But from where do we get this high-entropy randomness data? How do we safeguard data and keep it reliable then? With all said, there are actually only a couple of safe ways to accomplish this:
First, we can use data from API (for example, you can even take random.org perfect-level entropy atmospheric noise data or get it from your pet cat 🐈 even — as described in proofof.cat, via using ESP32 & Raspberry Pi kit) and connect them to a smart contract using Chainlink Oracle we described earlier. Read more about how to connect APIs to smart contract with Chainlink here!
Second — use a ready-made solution, namely Chainlink VRF, which is considered the most reliable solution for such purposes!
And the third— ask users to “add/gift” some randomness — similar to the KZG Ceremony or SSH Key generation | Image
On-chain-based “randomness” (based on gas cost, block hash, and stuff like that) is unreliable, it can be bypassed (e.g., one can buy specific NFT). Read more about it here! | Check this out as well.
Verified Random Function — provides cryptographically secure randomness using a set of Chainlink nodes: they feed data into smart contracts while maintaining the classic consensus mechanism through the committee of these nodes.
By offering Random Number Generation (RNG) for smart contracts, Chainlink VRF helps developers improve interaction by using random results in their blockchain-based applications.
How does the RNG work, and how is it verifiable? tl;dr; some magic is done with the usage of elliptic curves. This post has the best description, check it out!
Chainlink’s Verified Random Function (VRF) relies on a decentralized oracle network (DON) to enhance existing blockchains by providing verified off-chain data.
Moreover, tamper-proofed randomness with Chainlink VRF cannot be manipulated by any node operator, user, or even attacker, as each oracle in DON has an associated private/public key pair where the private key is maintained off-chain, while while the public key is published on the network.
With all said, Chainlink VRF is mostly used for:
NFTS and other blockchain-based games;
Randomly distributing responsibilities and assets. Check out this thread for a better understanding! We also want to note that LobsterDAO would be an excellent example of such a project. Check out contracts here as well. | LobsterDAO Chat & TLDR;
Selection of a representative sample regarding the consensus procedure. For example, VRF is used in the consensus methods of a variety of layer-1 blockchains, including Algorand, Cardano, Internet Computer, and Polkadot development!
It enables smart contracts to access randomness without compromising on security or usability. Chainlink VRF follows the Request & Receive Data cycle which works as stated: we send an oracle a request in one transaction, and the oracle, in turn, responds with the requested data in another transaction.
It is also important to mention that, for each request, Chainlink VRF generates one or more random values and cryptographic proof of how those values were determined.
This cryptographic proof (like the entire Chainlink ecosystem) is decentralized, which means that a centralized node cannot corrupt any information.
Ideally, smart contract should only consume the random number after consuming the cryptographic proof of the randomness of that number. It is important to deliver a proof, that is, a guarantee to the community that the raffle result is true!
Following the tips below can significantly improve the security of your project’s integration!
Use requestId to match randomness requests with their fulfillment in order.
Choose a safe block confirmation time, which will vary between blockchains. In theory, the following attack can happen: the miner/validator notices that Chainlink sent randomness→ they do not like this randomness (it is not profitable)→ they change the blockchain so that the original randomnessRequest got into another block (or the blockHash became different in that block). Because of this, the randomness is now different (can’t make it to be the desired randomness, they can just re-roll the randomness). It also is worth noting that the larger the confirmation block, the more difficult the theoretically possible attack.
Do not ever re-request randomness! Suppose there is logic in the contract that allows you to re-roll randomness in case the Chainlink does not respond after N blocks. The point is that if there is such logic, the Chainlink Oracle (VRF service provider, the one who generates random) can abuse it with not responding to the request and re-rolling a new random number. And also, if fulfillRandomWords makes a call to other addresses (for example, through a callback in safeTransferFrom), then the user can also re-roll it making a revert in a contract.
The fulfillRandomWords function must not revert. If your fulfillRandomWords() implementation reverts, the VRF service will not attempt to call it a second time. Make sure your contract logic does not revert. In the previous check this aspect is described as with re-request logic, the user can re-roll randomness.
A contract that uses Chainlink VRF must be inherited from the underlying VRFConsumer classes. (VRFConsumerBaseV2 for subscription, VRFv2WrapperConsumer for direct funding). There is a built-in check that rawFulfillRandomWords is called by the Chainlink contract.
Don’t accept bids/bets/inputs after you have made a randomness request. In theory, a transaction from a Chainlink could be frontrunned (see: Awesome MEV Resources & MEV101) when the transaction appears in a mempool and the randomness is known, accordingly.
The fulfillRandomWords should not consume too much gas, the gas that is specified in the requestRandomWords may not be enough, and this randomness request will be lost — keep this in mind!
Docs & Developer Resources:
VRF v2 announcement & introduction
Learn more about how to use & test it from this awesome repo made by LearnWeb3DAO!
Tl;dr for unique V2 features:
V2 has a new subscription mode;
V2 is way more configurable than V1 (IMO);
V2 has an increased gas limit. VRF V1 had 200k gas limit; V2 has 2m gas limit);
V2 has less gas usage (in subscription mode only);
1. State-dependent Randomness:
function fulfillRandomness(bytes32 requestId, uint256 randomness)
internal override {
Sacrifice storage currentSacrifice = ActiveSacrifices[requestId];
if (currentSacrifice.status != 1) {
return;
}
currentSacrifice.status = 2;
currentSacrifice.randomness = randomness;
}
function remainingShadowcornEggs()
public view
returns (uint256[3] memory, uint256)
{
uint256[3] memory remainingShadowcornEggsByType;
uint256 totalRemainingShadowcornEggs;
for (uint256 i = 0; i < 3; i++) {
uint256 availableCapacity = maxAmount[i]-
shadowcornContract.poolSupply(ShadowcornPoolIds[i]);
totalRemainingShadowcornEggs += availableCapacity;
remainingShadowcornEggsByType[i] = availableCapacity;
}
return (remainingShadowcornEggsByType, totalRemainingShadowcornEggs);
}
function completeSacrifice() external nonReentrant {
bytes32 requestId = CurrentSacrificeForSacrificer[_msgSender()];
require(requestId != 0,"sender has no sacrifice in progress");
(remainingShadowcornEggsByType,
totalRemainingShadowcornEggs
) = remainingShadowcornEggs();
Sacrifice storage currentSacrifice = ActiveSacrifices[requested];
uint256 initialRandomness = currentSacrifice.randomness % 100;
uint256 secondaryRandomness = currentSacrifice.randomness >> 7;
if (currenSacrifice.type == 1 || initialRandomness < 50) {
uint256 eggNumber = secondaryRandomness % totalRemainingShadowcornEggs;
if (eggNumber < remainingShadowcornEggsByType[0]) {
terminusPoolId = ShadowcornPoolIds[0];
} else if /** check for other types**/
//....
//.....
shadowcornContract.mint(_msgSender(), terminusPoolId, 1, ""); //ERC1155 mint
}//.....
delete CurrentSacrificeForSacrificer[_msgSender()];
emit SacrificeCompleted(_msgSender(), terminusPoolId, requestId);
}
Simplified:
tokensRemaining
), which means when other users call completeMint
, random changes for the user; because of this, the user can wait until the random is favorable and, only then, submit the completeMint
transaction.function fulfillRandomness(bytes32 requestId, uint256 randomness)
internal override
{
Sacrifice storage currentRandomness = ActiveRequests[requestId];
currentRandomness.randomness = randomness;
currentRandomness.randomnessFulfilled = true;
}
function completeMint() external nonReentrant {
bytes32 requestId = CurrentRequestOfUser[_msgSender()];
uint256 rawRandomNumber = ActiveRequests[requestId].randomness;
uint256 tokensRemaining = rareTokensLeft + commonTokensLeft;
require(tokensRemaining > 0, "No tokens left");
uint256 random = rawRandomNumber % tokensRemaining;
// chances to receive rare: rareLeft/total, for common: commonLeft/total;
if (random < rareTokensLeft) {
rareTokensLeft--;
mintRare();
} else {
commonTokensLeft--;
mintCommon();
}
delete CurrentSacrificeForSacrificer[_msgSender()];
emit SacrificeCompleted(_msgSender(), terminusPoolId, requestId);
}
2. Re-Rolling Randomness:
In this example, the user can make the smart contract as NFT receiver, and inside onERC721Received , revert the transaction if the token dna is unfavorable. Due to randomness retry logic, user can keep re-rolling the randomness until the desired dna is obtained.
contract Hatchery {
function fulfillRandomness(bytes32 requestId, uint256 randomness)
internal
override
{
Hatching storage currentHatching = hatchingByRequest[requestId];
currentHatching.status = Status.Finished;
uint256 tokenId = currentHatching.tokenId;
setDna(tokenId, randomness);
token.safeTransferFrom(address(this), owner[tokenId], tokenId);
}
function retryHatching(uint256 tokenId) external {
Hatching storage currentHatching = hatchingByRequest[requestByToken[tokenId]];
require(currentHatching.status == Status.Active &&
currentHatching.randomnessRequestBlockNumber < block.number + 30,
"retry hatching not available");
LibHatching.retryHatching(tokenId);
}
}
3. EVILink Chainlink Oracle Manipulation:
Demystifying Pythia: A Survey of ChainLink Oracles Usage on Ethereum
This project demonstrates a basic Hardhat use case. Connet api to SC using Chainlink!
This repo contains the Chainlink core node and contracts. The core node is the bundled binary available to be run by node operators participating in a decentralized oracle network.
This is a list of links and pages that you might need to help you throughout your learning journey — made by Chainlink
Learn more about how to use & test it from this awesome repo made by LearnWeb3DAO!
I’ve compiled some fantastic resources for you on how to properly run and integrate Chainlink VRF:
We hope that this article was informative and useful for you! Thank you for reading!
What instruments should we review? What would you be interested in reading about?
Support is very important to me, with it I can do what I love — educating users!
If you want to support my work, you can send me a donation to the address:
0xB25C5E8fA1E53eEb9bE3421C59F6A66B786ED77A or officercia.eth — ETH, BSC, Polygon, Optimism, Zk, Fantom, etc
4AhpUrDtfVSWZMJcRMJkZoPwDSdVG6puYBE3ajQABQo6T533cVvx5vJRc5fX7sktJe67mXu1CcDmr7orn1CrGrqsT3ptfds — Monero XMR