Oracles, Entropy & Chainlink VRF Secure Integration Tips

Greetings dear readers!

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!

We understand and respect your limited time, therefore we created a specific article with nothing superfluous for an easier access!

Introduction

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.

Blockchain Oracles Breakdown

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:

arxiv.org/pdf/2106.09349.pdf
arxiv.org/pdf/2106.09349.pdf

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!

Entropy VS Randomness

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:

How Low Entropy May Cause Significant Money Losses

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:

Randomness Meets Web3

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:

github.com/LearnWeb3DAO/Chainlink-VRFs
github.com/LearnWeb3DAO/Chainlink-VRFs

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:

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!

V1 vs V2

Docs & Developer Resources:

Tl;dr for unique V2 features:

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:

  • In this example, the randomness depends on the state variable (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:

  • It shows that a malicious miner still has a slim chance to tamper randomness provided by Chainlink’s VRF solution! Check out this demonstration of how one can manipulate VRF randomness:

IV — Resources

Oracles:

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:

Stay safe!

Subscribe to Officer's Blog
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.