đ° so you want randomness in your smartcontract
Assumed Audience: basic understanding of programming, likely designing your own project, likely already have questions on how to ârandomizeâ smart contract output.
Update: OOF did I miss a few important options here! Iâm working on improvements. In the meantime, here are some resources:
- https://our.status.im/two-point-oh-randomness/
- https://mvpworkshop.co/blog/ethereum-merge-everything-you-need-to-know/#DIFFICULTY_opcode_is_now_PREVRANDAO
- https://blockdoc.substack.com/p/randao-under-the-hood
- https://twitter.com/hasufl/status/1571864292571357184?t=G5nbgyG9tD9feohb9SMDrA&s=19
Continue to read the rest of this post! Use it to sharpen your thinking: do you know better ways to randomize? Reach out with your ideas!
While working on đ ď¸ Pathfinder, I hit the following conundrum:
I want each line of the poem to be a surprise until itâs revealed, but I also want 100% of my logic to remain on-chain.
Surprise in the context of code relies on random number generators. Randomness relies on secrecy. If you can predict a dice roll with a math equation, those dice are no longer random to you. The blockchain, however, is the opposite of secret. All inputs, outputs, and function logic are visible for everyone to see. The math equation and all its inputs, in other words, is there. So what do you do?
1. Outsource the randomness
For unpredictable randomness, you must reach outside the blockchain. Chainlinkâs VRF contract does that for you. When your contract calls Chainlinkâs, Chainlink runs something off-chain, and returns random values back to you.
Note that Chainlink charges for use of the VRF contract. According to their documentation, youâre charged for the gas to make the request, verify the random values, and send the values back to you. Presumably there is also some premium, but the documentation isnât clear.
When to use it
Because youâre being charged gas fees for 3 different actions, calling VRF can get expensive. When gas fees are low, getting the random values back can cost less than $2. But when theyâre high, prices can jump to $30+. And again, thatâs only one of the three gas fees youâre paying. Still, the cost is worth it if a hack1 can destroy your ecosystem.
Consider VRF if youâre building:
- a financial application or any game/raffle/lottery with a monetary prize
- digital assets with high expected monetary value.
- For example: Loot launched without any noticeable exploits to its not-so-random number generator. However, once the monetary value of Loot skyrocketed, copycats and add-ons launched, many copying the same pseudo-random number generator. With Lootâs value so high, these add-ons caught peoplesâ eye and exploits increased significantly. Similarly, if you expect your project to have large monetary value at mint time (e.g. a BAYC add-on), go with VRF.
- any game where fair randomness is paramount to a good play experience
- For example: on-chain Monopoly would flop if the dice rolls werenât actually random.
VRF may not be worth it if:
- the expected floor for your token is low or you expect to mint out under the radar
- Again, note that Lootâs pseudo-random number generator wasnât hacked. If youâre launching a project with a low mint price or otherwise feel hackers wonât consider your project âworthâ exploiting, you can save money by skipping VRF.
- itâs not worth the gas fee fluctuations.
- If youâre using randomness for a low-stakes part of your system, $30 in gas fees may not be worth it.
- philosophically, you want to keep everything on-chain
What did I choose?
đ ď¸ Pathfinder is a small art project questioning our concept of ownership by having the public excavate a poem together. VRF costs didnât seem worth it: especially given gas price volatility.
2. Provide your own randomness
VRF isnât the only way to get randomness on-chain. The pattern is one you could follow in your own dAppâ as long as you have a server running. You could do this in two ways:
- Follow the ârequestRandomValuesâ, âreturnRandomValuesâ pattern. Except your server is listening for requestRandomValues events and calling returnRandomValues on your contract.
- Require calls to the random-requiring function on your contract to come only through your dAppâs frontend. Your server can then generate a random value via its native library and pass that into the contract.
- e.g. if minting relies on a random number generator, folks can only mint through your dApp.
When to use it
This still costs gas, but probably less than VRF. However, keeping the dApp servers up and running will also cost you money. Whether this approach or VRF is cheaper will depend on whether you need a server for other reasons.
Consider if:
- any of the VRF âconsider ifâ conditions above apply.
- you already plan to run your dApp on a server.
- the âverifiably fairâ part of VRFâs system either isnât useful to you or is something you could recreate easily
It may not be worth it if:
- you want your contract to be directly callable
- you want the blockchain to be your only backend (aka you donât plan on having a server of your own running)
- none of the VRF âconsider ifâ conditions above apply
- any of the VRF ânot worth it ifâ conditions above apply
What did I choose?
I wasnât planning on running a server, and I want đ ď¸ Pathfinder to be accessible directly from the contract: not just from my own frontend. So I chose not to use this approach.
3. Embracing pseudo-randomness
Options 1 and 2 get you the gold standard in computer-based randomness. If you donât want that, youâll need to roll your own silver standard. The guiding principle is: âLimit the callerâs control and the minerâs control.â
Limiting the callerâs control is easy: donât use user inputs in your random number generator.2 However, there are still two ways callers can hack your randomness:
- Paying the miner/validator to use a specific value in their block
- Paying the miner/validator to wait until value they want comes along
You can prevent #1 by avoiding block properties miners can control like block.timestamp. Instead, consider values like block.number or block.difficulty. With the transition to Proof of Stake, Ethereum now provides a pseudorandom number with each block using the randao system. (If you already deployed a contract relying on block difficulty, your contract will now get the randao value instead.)
#2 is unavoidable, but is inherently very expensive. Plus, you can make waiting inconvenient, or use other people to add additional unpredictability. Weâll get into that shortly.
Again, if you really need randomness, use one of the other approaches. Otherwise, making an exploit inconvenient or expensive may be good enough.
When to use it
On-chain pseudo-randomness works if:
- the mechanic that uses randomness is not designed to have a huge impact
- Maybe your project is tiny and not designed to moon. Or maybe you want semi-random way to decide if a dragon will be blue or green but the color really doesnât impact the game downstream. Basically cases where you believe gaming the system isnât that big a deal at the end of the day.
- you plan on revealing the impact of the randomness later
- You could assign a pseudo-random number to each NFT at mint time, for example, but only upload the metadata for the NFTs after all tokens are minted. That way, users donât have enough data to game the system.
- it fits the lore of your project
- Corruption(s*) is an NFT collection where each token has âinsightâ. Insight relies on block number, as well as when the block was last transferred: two values outside the owner and minerâs control. It changes over time: growing as the token is held, and shrinking when the token is disturbed.
- While this avoids issue 1 (itâs not controllable by miners), it cannot avoid issue 2 (in that users know the results of their actions in advance). But it doesnât matter because the tokenâs lore is such that it doesnât need to be purely random. It just needs to grow and shrink in slightly uncontrollable ways.
It does not work if:
- Unpredictable, uncontrollable randomness is crucial to your project, as discussed thoroughly above.
What did I choose?
On-chain pseudo-randomness felt right for đ ď¸ Pathfinder. Especially since I do want participants to see the connection between their actions and the final poem. I just donât want them to feel fully in control.
I tried to balance the issues above as follows:
- It doesnât use many user-controlled inputs.
- The newHistoricalInput function is where randomness is created. Youâll notice that most values are not controlled by the user or the miner: block.number, block.difficulty.
- Public addresses, however, are also used, making the final output somewhat within a userâs control. These fit Pathfinderâs lore (more below), but for your project, you can drop those to prevent user manipulation.
- Itâs part of the lore
- Pathfinder is a collaborative community project. The whole point is that your interactions with the contract impact the poem. Incorporating values like a userâs public address allows each user to become part of the poem itself.
- While users can calculate their impact on the historical input in advance, thatâs part of what makes the project community-driven. If someone goes to extreme lengths to get the exact output they want, I consider that part of the collaboration.
- The community adds randomness
- Unlike Corruption(s*), where each token has its own pseudo-random value, Pathfinder has one random value controlling the poem that can be modified by any token holder at any time.
- This means while you can try to force the poem to go where you want, you canât control your fellow participants! One person submitting a transaction before you will throw off all your calculations.
So what should you do?
Which option you choose is up to you and your project. If unpredictable, untamperable randomness is of paramount importance: look into VRF or a dApp of your own! And if itâs not, dig into the story behind your project and decide: which block properties help you best tell that story? And how can you bring them together into something meaningful?
Info:
- Inspired by: đ ď¸ Pathfinder
- Related to:
- [[đ° so you want to deploy a smart contract]]
- Updates:
- 2022-09-18
- Added details on randao and prevrandao
- Removing the Mannyâs game statement about needing to pay 2 miners, as thatâs incorrect.
- 2022-09-18
-
In this piece, I refer to any user circumventing your desired randomness as a âhackâ and any such user as a âhackerâ. These terms are used loosely and to denote that the outcome is not what you desired. ↩
-
This is where Loot made its mistake. The random number generator uses tokenId, but the public
claim
function allows users to decide which tokenId theyâd like to mint. Users could run the code in advance to decide which tokenId would get them the rarest combination and mint that specific token. ↩
Every post on this blog is a work in progress. Phrasing may be less than ideal, ideas may not yet be fully thought through. Thank you for watching me grow.
Updates
- : Add disclaimer that I missed some stuff