# How to debug a Chainlink VRF tx

By [Clemlaflemme](https://paragraph.com/@clemlaflemme) · 2022-05-10

---

How to debug a Chainlink VRF v2 transaction, a step-by-step tutorial
--------------------------------------------------------------------

Recently, I released a new on-chain CC0 NFT project called the [co-bots](https://gist.github.com/ClementWalter/co-bots.com). This project has also a specificity: during the minting process, at some given checkpoints, it randomly rewards on of the early minters with a giveaway in ETH taken from the contract balance.

The launch of this project was last Friday. Right after the launch, and despite a thorough testing (both local unit-test, rinkeby and external audit of the contract) we faced a bug in the front-end regarding a discount mechanism. In other words tensions was palpable in the dev team!

But still people were able to mint. Approximately one hour after the launch, while we were still trying to figure out the bug with matog.eth, the first checkpoint was reached. Good vibes expected! But, _omg_, the first random actually was, wait for it, 0. And, wait for it, we took for ourselves, right before the launch, one bot each, precisely the three first token (#0, #1 and #2).

In short, in a random draw mechanism whose advocated purpose was to reward the community, we actually ended up taking for ourselves with a very _strange_ result the first 1 ETH giveaway, picking _randomly_ the token #0.

_Does this sound right to you?_ _Are we playing again some sort of AkuAuction bug?_ _Is this a rugged bug?_

Eventually the project has been almost stalled since then, and I do understand the emotional impact of this situation. Furthermore, the world "bug" was tweeted by smlg.eth in order to explain this before I had the time to deep dive into the Chainlink VRF v2 mechanism to provide a comprehensive proof of this as a real and honest while unexpected result.

The purpose of this article is hence to showcase how to read Chainlink VRF v2 transactions. This took like a dozen of minutes to figure out, probably 12 minutes too much!

Chainlink VRF v2 mechanism
--------------------------

The [Chainlink VRF v2 mechanism](https://docs.chain.link/docs/chainlink-vrf/) is an oracle used to retrieve a **verifiable random number** in a smart contract. It is [before the merge](https://ethereum-magicians.org/t/eip-4399-supplant-difficulty-opcode-with-random/7368) the only way to get true random numbers on-chain.

Since this is an oracle, requesting a random number requires indeed two transactions:

*   the first one from the smart contract to the oracle, requesting a random number
    
*   the second one from the oracle to the smart contract, providing the random number
    

When using the Chainlink VRF v2 oracle, the callee inherits from `VRFConsumerBaseV2` and implements the dedicated `fulfillRandomWords` method. This method is called by the oracle when fulfilling the request, ie. providing the actual random number.

Hence, while the first transaction is an outbound transaction easily visible in etherscan, the second one is somehow hidden and needs to be parsed accordingly.

The next section gives, using the CoBots example, a step-by-step guide to analyse the oracle callback transaction and consequently observe the confusing #0 draw.

A step-by-step guide
--------------------

### Chainlink subscription

A caller contract using the Chainlink VRF v2 oracle is called the **consumer**. It needs to subscribe to the oracle to be able to make any request.

Digging into the "read" view of the [CoBots contract on etherscan](https://etherscan.io/address/0x78Fc2f8CBe43B02Beff806B4F0AEb1EEb0a11894#readContract), one can find the following variable:

    chainlinkSubscriptionId: 117
    

Note the contract may not make this value public though. But I guess that Chainlink’s VRF purpose is transparency so why keeping it private?

This subscription id can be used to head up to the subscription dashboard, managed by Chainlink itself:

    https://vrf.chain.link/mainnet/117
    

The dashboard let monitor the subscription: add funds, cancel, see history of requests.

### The caller's request

At the time of writing this article, only one checkpoint was drawn so the dashboard history tab displays only one entry. Clicking on the consumer address, one is redirected to the consumer's Etherscan page, ie. the [CoBots V2 contract](https://etherscan.io/address/0x78Fc2f8CBe43B02Beff806B4F0AEb1EEb0a11894#code).

![The Chainlink VRF dashboard for the Co-Bots's subscriptionId](https://storage.googleapis.com/papyrus_images/4e0ac2ac2f1db936563b02cfc48dfbfb0d9f7cf8c4af92445d654c15f692e77b.png)

The Chainlink VRF dashboard for the Co-Bots's subscriptionId

Here we know already that this `chainlinkSubscriptionId` is not fake! And the contract actually called the oracle.

Looking at the [transactions tab](https://etherscan.io/address/0x78Fc2f8CBe43B02Beff806B4F0AEb1EEb0a11894) of the contract on etherscan, one can find the transaction that actually triggered the oracle, called `draw`. More precisely, in the [log page](https://etherscan.io/tx/0x0859703a8d40d91abe960aa2a29bd2a90cf0fda6e122013cbfac8b1d395ae478#eventlog) of the transaction, one can find the event emitted by the Chainlink coordinator.

All-in-all, there is no doubt here that the contract actually called the oracle. And that the oracle actually received the request. And that the corresponding entry in the dashboard is the one we thought it was (ouf!).

### The oracle's response

Back on the Chainlink dashboard, there is a "Transaction hash" reported for the request. This is the hash of the second transaction.

This transaction is actually an _internal Chainlink transaction_ in the sense that it does not call the callee directly. Indeed, [back on etherscan](https://etherscan.io/tx/0xdd1a5fb9940a278bd8cc077b90187af2eb0c67ab1b811ed89cb04b6ed1d56902), one sees that it comes from an unknown account to the Chainlink coordinator.

So the coordinator is called back and one can decode the input data of the transaction, ie. figure out which function the oracle called, and the parameters it received.

The input data as it reads on etherscan is:

This input data can “easily” be decoded: the four first bytes (`af198b97`) are the function selector, and the rest are the parameters. Hence the function is indeed

    function fulfillRandomWords(Proof memory proof, RequestCommitment memory rc)
    

In the body of the function, one can find the following code:

    bytes memory resp = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, requestId, randomWords);
    

which is exactly where our contract gets called.

Note: instead of decoding the input data, a naive search for `rawFulfillRandomWords` could have done the job since it’s called only once.

**But where is this transaction visible?**

### Alchemy composer

The internal transactions tab displays the transactions that are triggered by another contract. Eventually here we find [the transaction](https://etherscan.io/tx/0xdd1a5fb9940a278bd8cc077b90187af2eb0c67ab1b811ed89cb04b6ed1d56902) that we previously mentioned.

We see that this transaction actually led to a transfer of 1 ETH, but we cannot see the actual value of the random number received by the contract.

To get more information, we can use the [Alchemy Composer](https://composer.alchemyapi.io/) and its `trace_transaction` method.

Inputting the given transaction hash, we get as an output a list of all the `calls` made by the transaction. If we `Ctlf+f` our CoBots V2 contract address (`0x78fc2f8cbe43b02beff806b4f0aeb1eeb0a11894`, beware of the case) we find one specific entry:

    {
      "action": {
        "from": "0x271682deb8c4e0901d1a1550ad2e64d568e69909",
        "callType": "call",
        "gas": "0x7a120",
        "input": "0x1fe543e3da6e6014fa83d1f504b2de027084f110ef375c5f919d4f24dabd3ff824f5b7e40000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000154210d9c7178a0af88fe45b66ff56d38d2f634b555daf8dbf55e9a0b16f2b718",
        "to": "0x78fc2f8cbe43b02beff806b4f0aeb1eeb0a11894",
        "value": "0x0"
      },
      "blockHash": "0x9779d03ecd404575cb926f1141e8226b54920e992591712a9933549514aeac7e",
      "blockNumber": 14724644,
      "result": {
        "gasUsed": "0x106b5",
        "output": "0x"
      },
      "subtraces": 1,
      "traceAddress": [
        0
      ],
      "transactionHash": "0xdd1a5fb9940a278bd8cc077b90187af2eb0c67ab1b811ed89cb04b6ed1d56902",
      "transactionPosition": 42,
      "type": "call"
    }
    

Bingo! The "input" field is the input data of out own `rawFulfillRandomWords` function.

Once again, we can decode it:

    // function selector
    1fe543e3
    // requestId
    da6e6014fa83d1f504b2de027084f110ef375c5f919d4f24dabd3ff824f5b7e4 
    // pointer to memory for arg randomWords[]
    0000000000000000000000000000000000000000000000000000000000000040 // length of randomWords
    0000000000000000000000000000000000000000000000000000000000000001 // the random value randomWords[0]
    54210d9c7178a0af88fe45b66ff56d38d2f634b555daf8dbf55e9a0b16f2b718
    

Given these values as `bytes`, the verifier (any anon that followed me so far) can find back values found in the contract. Opening the console of their preferred browser (the one use right at this moment), they can see that:

    > BigInt("0xda6e6014fa83d1f504b2de027084f110ef375c5f919d4f24dabd3ff824f5b7e4").toString()
    '98799217301508220758223172538644033103911863065754175684428858404639975258084' // requestId as visible on the contract page
    
     > BigInt("0x54210d9c7178a0af88fe45b66ff56d38d2f634b555daf8dbf55e9a0b16f2b718").toString()
    '38052679174536163966270071048856216962117352358951829765782333889304992069400' // randomValue
    

Because the checkpoint for this draw was `100`, we picked the `randomValue % 100` as the winning token, and eventually got `#0000`, OMG!

Conclusion
----------

Though it is still a bit challenging to use Chainlink VRF (see also my [post](https://mirror.xyz/clemlaflemme.eth/v3O2NPRW75U5s5NG5CgsFlvNNw-TEFXB3LNjwaGqTI0) about unit-testing it), it is really worth both for reassuring the community and the devs when something goes against what seems "normal".

In the meantime, the Co-Bots contract has still ~4 ETH locked waiting for the next draw!

Don’t hesitate to have a look at the [co-bots monorepo](https://github.com/ClementWalter/co-bots-monorepo) and feel free to reach out to me on [Twitter](https://twitter.com/ClementWalter) or [Discord](https://discord.gg/BtuD5HguDX) should you have any question or suggestion!

---

*Originally published on [Clemlaflemme](https://paragraph.com/@clemlaflemme/how-to-debug-a-chainlink-vrf-tx)*
