Search code examples
soliditychainlink

Chainlink VRF or RANDAO?


    bytes9 private _randomness;

    function getRandomness() public view returns (uint256) {
        return uint256(keccak256(abi.encode(_randomness, address(this))));
    }
    
     modifier updateRandomness() {
        bytes32 randomness = _randomness;
        assembly {
            // Pick any of the last 256 blocks psuedorandomly for the blockhash.
            // Store the blockhash, the current `randomness` and the `coinbase()`
            // into the scratch space.
            mstore(0x00, blockhash(sub(number(), add(1, byte(0, randomness)))))
            // `randomness` is left-aligned.
            // `coinbase()` is right-aligned.
            // `difficulty()` is right-aligned.
            // After the merge, if [EIP-4399](https://eips.ethereum.org/EIPS/eip-4399)
            // is implemented, the randomness will be determined by the beacon chain.
            mstore(0x20, xor(randomness, xor(coinbase(), difficulty())))
            // Compute the new `randomness` by hashing the scratch space.
            randomness := keccak256(0x00, 0x40)
        }
        _randomness = bytes9(randomness);
        _;
    }

    function generateNFT() external updateRandomness {
        uint256 randomNum = getRandomness();
        uint256 remaining = MAX_SUPPLY - totalSupply();
        uint256 newId = (randomNum % remaining);
        // ...
    }

seems like after the merge, getting a solid random value might be feasible.

this could be a good replacement for chainlink vrf?


Solution

  • Summary

    For pseudorandomness you can use something like EIP-4399:

    uint256 randomness = uint(keccak256(abi.encodePacked(msg.sender, block.difficulty, block.timestamp)));
    

    (You don't even need to use assembly, difficulty is directly exposed in solidity)

    But for true randomness, you'd need something like Chainlink VRF.

        function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override {
            uint256 randomness = _randomWords[0];
        }
    

    Why is difficulty or PREVRANDAO pseudo-random in a post-merge ETH?

    When a validator is due to propose a block, they face a fairly minimal penalty for failing to do so. Given a smart contract that is trying to make itself unpredictable/uncontrollable via PREVRANDAO, this option allows them to cheaply bias the behavior of the contract in the following ways.

    1. You always need a validator to propose two blocks in a row

    PREVRANDAO's value has no entropy given the PREVRANDAO value from the previous block if no block is proposed. When you propose a block, that's a piece of the math equation that PREVRANDAO uses to make a random number. So if you don't propose a block, you get a "less" random number.

    So users of PREVRANDAO would need to check that a validator has provided a block since the last time they called PREVRANDAO. Otherwise, they won't necessarily be drawing statistically independent random outputs on successive calls to PREVRANDAO.

    This means that the validator committed to proposing a particular block can effectively set the PREVRANDAO value for the next block to two possible values:

    • the one which results from its mandated PREVRANDAO input value
    • whatever PREVRANDAO gets deterministically set to if no block is proposed

    This choice alone allows validators to influence the random number, no longer making the number random.

    Even if the contract does something cleverer, like taking the PREVRANDAO output from the first block to be proposed after some height, every validator after that height has the same option. No matter which way the contract tries to access PREVRANDAO, the last validator to contribute will always have predictable control over the random output which controls the contract.

    These issues don't arise for Chainlink VRF, because the output is deterministic, given the blockhash (just infeasible to compute for anyone who doesn't know the secret key.)

    2. Validators can choose to not post if a number is unfavorable

    Once again, you still run into this issue from pre-merge. The penalty of not posting a block is almost negligible, so a node might be financially incentivized not to propose blocks in a lottery-type smart contract if the PREVRAND value isn't what they want. Enough nodes do this and you run into issues.

    Just taking the current PREVRANDAO value without looking at recent history gives a validator some control over the output the contract will use, even if it doesn't want to abort, because the validator can decide whether or not the block it proposes contains a transaction which will trigger the use of PREVRANDAO. The only cost involved there is the transaction fee/tip.

    3. If all applications use PREVRANDAO as their seed

    If all applications use PREVRANDAO as their seed, in a way, you could "chain" together wins or hacks based on this.

    More information

    There is also an interesting thread on this in the Ethereum Magicians forum.