Exploiting Predictable Addresses in Solidity


Today, let’s take on a quick Solidity challenge and unravel a smart contract with funds up for grabs. The catch? The prize is seemingly protected by a random function. But there’s a flaw. Can you spot it?

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract VulnerableEntropy {
    uint256 public randomNumberInit;

    constructor(uint256 _randomNumberInit) payable {
        require(msg.value >= 1 ether, "Not enough ETH"); // Deployer must send at least 1 ETH
        randomNumberInit = _randomNumberInit; // Set the random number for some entropy
    }

    function tryWinPrize() public {
        require(generateRandomNumber() == 0, "Not lucky enough"); // Check if the random number is 0
        payable(msg.sender).transfer(address(this).balance); // Send the prize to the winner
    }

    function generateRandomNumber() private view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(block.timestamp, randomNumberInit, msg.sender))) % 999;
    }
}

The Flawed Random Function

Here’s the challenge: a contract is initialized with some funds. Participants can interact with it to win these funds. However, the prize is guarded by a random function that’s not as random as it seems.

Deconstructing the Random Function

Let’s break down the elements we can control in the contract:

  • block.time: While we can wait to alter this, others might get lucky and win first.
  • RandomNumberInit: This is fixed and visible in the deployment logs.
  • msg.sender: Here’s the catch! It’s fixed to our address, but we can manipulate it using the create2 opcode, which allows deploying contracts at predictable addresses.

create2 Documentation

Using create2 to deploy the proxy contract

The create2 opcode specifies that a new contract will be deployed at an address derived from keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]. To exploit this, we need:

  • The bytecode of our proxy contract.
  • The address of the vulnerable contract.
  • A salt to brute-force the desired address.

We create a proxy contract to call the tryWinPrize function for us. Our goal is to deploy it at an address that satisfies the random function condition.

Proxy Contract

The Attacker Contract

Next, we need a contract to deploy our proxy. This contract will brute-force the salt used at deployment to precompute the address.

To precompute the address, we pass the bytecode of our proxy contract, and the salt that we want to use:

Finding the Salt

Using a loop, we compute the address for each new salt, until we find one that passes the generateRandomNumber function’s test.

Attacker Contract

Testing and Execution

Using Foundry, we set up the test:

  • Deploy the vulnerable contract.
  • Deploy the attacker contract.
  • Call attack, which brute-forces the best address and deploys the proxy, triggering tryWinPrize successfully.

Foundry Test

Run the test, and voilà – we deployed the proxy at the correct address and claimed the prize.

Foundry Test

Conclusion

This exercise demonstrates that using msg.sender as a source of entropy for random functions is risky and can be manipulated. Always consider potential vulnerabilities when dealing with randomness in smart contracts!