가위바위보 스마트 컨트랙트

임동규·2022년 4월 7일
0

web3

목록 보기
2/2
post-thumbnail

solidity 를 사용하여 가위바위보를 통해 이더를 딸 수 있는 간단한 스마트 컨트랙트를 작성해보겠다.

가위바위보 게임의 전체 플로우

  1. 방장(originator)이 createRoom 을 호출해서 방을 만든다. 방을 만든 후 가위/바위/보 값과 배팅금액을 넘겨줌.
  2. 참가자(taker)가 joinRoom을 호출시켜 방에 참여하고 가위/바위/보 를 낸다.
  3. payout 함수를 통해서 게임 승자에게 배팅 금액 송금

플레이어 설정

//SPDX-License-Identifier: MIT //MIT 라이센스
pragma solidity ^0.8.7;

contract RPS {
	constructor () payable {}

	enum Hand {  //플레이어가 낼 수 있는 가위바위보 값 
	 rock, paper, scissors
      //0   1      2
	}

	enum PlayerStatus {  // 플레이어의 상태
		STATUS_WIN, STATUS_LOSE, STATUS_TIE, STATUS_PENDING
           //0        1             2          3
  }

	struct Player {  //구조체 구조 = 타입 변수이름
		address payable addr;  //주소
		uint256 playerBetAmount; //배팅금액
		Hand hand;  //플레이어가 낼 가위바위보 값
		PlayerStatus playerStatus;  // 사용자의 현 상태	
	}	
}

게임 환경 셋팅

  enum GameStatus {
        STATUS_NOT_STARTED, STATUS_STARTED, STATUS_COMPLETE, STATUS_ERROR
    }

  struct Game {
        uint256 betAmount;
        GameStatus gameStatus;
        Player originator;
        Player taker;
    }



  modifier isValidHand (Hand _hand) {
        require((_hand  == Hand.rock) || (_hand  == Hand.paper) || (_hand == Hand.scissors));
        _;
    } //modifier 의 특징상 _hand 가 아닐경우 함수 밑의 코드가 실행안됨.
    
    modifier isPlayer (uint roomNum, address sender) {
        require(sender == rooms[roomNum].originator.addr || sender == rooms[roomNum].taker.addr);
        _;
    } //게임에 참여한 사람이 방을 만든 사람이 아니거나 입장한 사람이 아니라면 게임 진행안됨.
    

방생성

mapping(uint => Game) rooms; //mapping 을 이용하여 key 는 숫자 value는 만들어진 Game 으로 들어갈 수 있음.
uint roomLen = 0; // 처음 만들어진 방의 번호

function createRoom (Hand _hand) public payable isValidHand(_hand) returns (uint roomNum) { //createRoom 함수는 hand 라는 매개변수를 받고 roomNum 을 리턴할 것이다.
    rooms[roomLen] = Game({
        betAmount: msg.value, //트랜잭션의 value 값
        gameStatus: GameStatus.STATUS_NOT_STARTED, //방이 만들어지면 현재 게임 상태는 시작되지 않은 상태
        originator: Player({
            hand: _hand, //createRoom 을 호출할때 _hand 라는 매개변수가 hand Player 의 hand 값으로 들어옴.
            addr: payable(msg.sender), //createRoom 호출한 계정의 주소
            playerStatus: PlayerStatus.STATUS_PENDING, //현재 진행되지ㅣ 않았기에 pending 상태
            playerBetAmount: msg.value //만든사람이 배팅한 금액
        }),
        taker: Player({
            hand: Hand.rock, //일단 taker가 없으니 rock 으로 설정
            addr: payable(msg.sender),  
            playerStatus: PlayerStatus.STATUS_PENDING,
            playerBetAmount: 0
        })
    });
   roomNum = roomLen;  // roomNum은 리턴된다.
   roomLen = roomLen+1;  // 다음 방 번호를 설정
	}
}

게임 참여

 function joinRoom(uint roomNum, Hand _hand) public payable isValidHand( _hand) {
       
        //밑의 코드를 통해 createRoom 에서 생성한 Game 구조체를 변경한다.
        rooms[roomNum].taker = Player({
            hand: _hand, //참여자가 내는 가위바위보 값
            addr: payable(msg.sender), //참여자의 주소
            playerStatus: PlayerStatus.STATUS_PENDING, //비교하지 않았으니 PENDING 상태
            playerBetAmount: msg.value //참여자가 배팅한 금액
        });
        rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value; //매핑에 roomNum이라는 키를 통해 해당 Game 의 betAmount 를 수정
        compareHands(roomNum); //compareHands를 통한 가위바위보 게임 계산
    }

누가 이겼는지 비교하기

 function _compareHands(uint roomNum) private{
        uint8 originator = uint8(rooms[roomNum].originator.hand); //originator 라는 변수에 originator 의 hand 값 할당
        uint8 taker = uint8(rooms[roomNum].taker.hand); //taker 라는 변수에 taker의 hand 값 할당
        
        rooms[roomNum].gameStatus = GameStatus.STATUS_STARTED;
        //gameStatus 를 시작으로 변경
   		
   
   
   		//비교 시작해서 승자와 패자의 playerStatus 상태 변경
        if (taker == originator){ //draw
            rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
            rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
            
        }
        else if ((taker +1) % 3 == originator) { // originator wins
            rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_WIN;
            rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_LOSE;
        }
        else if ((originator + 1)%3 == taker){
            rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_LOSE;
            rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_WIN;
        } else {
            rooms[roomNum].gameStatus = GameStatus.STATUS_ERROR;
        }
       
    }

지불받기

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

contract RPS {
    
    
    
    constructor () payable {}
    
    /*
    event GameCreated(address originator, uint256 originator_bet);
    event GameJoined(address originator, address taker, uint256 originator_bet, uint256 taker_bet);
    event OriginatorWin(address originator, address taker, uint256 betAmount);
    event TakerWin(address originator, address taker, uint256 betAmount);
   */
   
    enum Hand {
        rock, paper, scissors
    }
    
    enum PlayerStatus{
        STATUS_WIN, STATUS_LOSE, STATUS_TIE, STATUS_PENDING
    }
    
    enum GameStatus {
        STATUS_NOT_STARTED, STATUS_STARTED, STATUS_COMPLETE, STATUS_ERROR
    }
    
    // player structure
    struct Player {
        Hand hand;
        address payable addr;
        PlayerStatus playerStatus;
        uint256 playerBetAmount;
    }
    
    struct Game {
        uint256 betAmount;
        GameStatus gameStatus;
        Player originator;
        Player taker;
    }
    
    
    mapping(uint => Game) rooms;
    uint roomLen = 0;
    
    modifier isValidHand (Hand _hand) {
        require((_hand  == Hand.rock) || (_hand  == Hand.paper) || (_hand == Hand.scissors));
        _;
    }
    
    modifier isPlayer (uint roomNum, address sender) {
        require(sender == rooms[roomNum].originator.addr || sender == rooms[roomNum].taker.addr);
        _;
    }
    
    
    function createRoom (Hand _hand) public payable isValidHand(_hand) returns (uint roomNum) {
        rooms[roomLen] = Game({
            betAmount: msg.value,
            gameStatus: GameStatus.STATUS_NOT_STARTED,
            originator: Player({
                hand: _hand,
                addr: payable(msg.sender),
                playerStatus: PlayerStatus.STATUS_PENDING,
                playerBetAmount: msg.value
            }),
            taker: Player({ // will change
                hand: Hand.rock,
                addr: payable(msg.sender),  
                playerStatus: PlayerStatus.STATUS_PENDING,
                playerBetAmount: 0
            })
        });
        roomNum = roomLen;
        roomLen = roomLen+1;
        
        
       // Emit gameCreated(msg.sender, msg.value);
    }
    
    function joinRoom(uint roomNum, Hand _hand) public payable isValidHand( _hand) {
       // Emit gameJoined(game.originator.addr, msg.sender, game.betAmount, msg.value);
        
        rooms[roomNum].taker = Player({
            hand: _hand,
            addr: payable(msg.sender),
            playerStatus: PlayerStatus.STATUS_PENDING,
            playerBetAmount: msg.value
        });
        rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value;
        compareHands(roomNum);
    }
    
    function payout(uint roomNum) public payable isPlayer(roomNum, msg.sender) {
        if (rooms[roomNum].originator.playerStatus == PlayerStatus.STATUS_TIE && rooms[roomNum].taker.playerStatus == PlayerStatus.STATUS_TIE) {
            rooms[roomNum].originator.addr.transfer(rooms[roomNum].originator.playerBetAmount);
            rooms[roomNum].taker.addr.transfer(rooms[roomNum].taker.playerBetAmount);
        } else {
            if (rooms[roomNum].originator.playerStatus == PlayerStatus.STATUS_WIN) {
                rooms[roomNum].originator.addr.transfer(rooms[roomNum].betAmount);
            } else if (rooms[roomNum].taker.playerStatus == PlayerStatus.STATUS_WIN) {
                rooms[roomNum].taker.addr.transfer(rooms[roomNum].betAmount);
            } else {
                rooms[roomNum].originator.addr.transfer(rooms[roomNum].originator.playerBetAmount);
                rooms[roomNum].taker.addr.transfer(rooms[roomNum].taker.playerBetAmount);
            }
        }
         rooms[roomNum].gameStatus = GameStatus.STATUS_COMPLETE;
    }
    
    function compareHands(uint roomNum) private{
        uint8 originator = uint8(rooms[roomNum].originator.hand);
        uint8 taker = uint8(rooms[roomNum].taker.hand);
        
        rooms[roomNum].gameStatus = GameStatus.STATUS_STARTED;
        
        if (taker == originator){ //draw
            rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
            rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
            
        }
        else if ((taker +1) % 3 == originator) { // originator wins
            rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_WIN;
            rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_LOSE;
        }
        else if ((originator + 1)%3 == taker){
            rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_LOSE;
            rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_WIN;
        } else {
            rooms[roomNum].gameStatus = GameStatus.STATUS_ERROR;
        }
       
    }
}

그러나 이 컨트랙트에는 문제가 있다.....

바로바로 트랜잭션은 블록에 기록이 되기에 createRoom 을 실행했을 경우 방생성자가 낸 가위바위보 값이 그대로 드러난다는 것이다.

이런식으로 createRoom 함수를 호출한 트랜잭션에 무슨 값을 냈는지 확인 할 수 있다.

그렇다면 이걸 보이지 않게하는 방법은 없을까?

하나의 방법이 있다.
내가 낸 가위바위보를 해시화 하는 방법이 있다.


  function keccak(uint256 _hand, string memory _key) public pure returns (bytes32) {
    return keccak256(abi.encodePacked(_hand, _key));
  }

keccak으로 해시를 해주면 된다.

hand 값과 salt 값을 함수의 인수로 넣어주면 해시값이 나온다.
1과 cake 를 해시한 결과값은 0xe54f62663dce36479166f39e7fe2ce1605a368c73f6bdff7fb2142a2f19f3133
이런식으로 나온다.

그리고 그 값을 CreatRoom 과 JoinRoom 함수에 uint 값 대신 해시값을 넣어주면된다.해시값은 Player.hand 필드에 저장되게 된다.

  function createRoom(bytes32 _hand) public payable returns (uint256 roomNum) {
    rooms[roomLen] = Game({
      betAmount: msg.value,
      gameStatus: GameStatus.STATUS_NOT_STRTED,
      originator: Player({
        hand: _hand,
        addr: payable(msg.sender),
        playerStatus: PlayerStatus.STATUS_PENDING,
        playerBetAmount: msg.value,
        result : 0,
        count : 0  //만약 count를 넣어주지 않는다면 상대방이 내가 뭘냈는지 확인후에 다시 낼 수 있을 것이다. 해시화를 한다고 하더라도 뭘로 해시햇는지 블록체인은 알 수 있기 때문이다.
      }),
      taker: Player({
        hand: 0,
        addr: payable(msg.sender),
        playerStatus: PlayerStatus.STATUS_PENDING,
        playerBetAmount: 0,
        result : 0,
        count : 0

      })
    });
    roomNum = roomLen;
    roomLen = roomLen + 1;
  }

  function joinRoom(uint256 roomNum, bytes32 _hand) public payable {
    require(msg.value == rooms[roomNum].betAmount);
    rooms[roomNum].taker = Player({
      hand: _hand,
      addr: payable(msg.sender),
      playerStatus: PlayerStatus.STATUS_PENDING,
      playerBetAmount: msg.value,
      result : 0,
      count : 0
     
    });
    rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value;
  }

이렇게 보내게 된다면

트랜잭션에는 내가 무엇을 냈는지 확인할수가없다.

이번에는 joinRoom 에 0.1이더와 1 과 coke를 해시화한 값을 보낼것이다.

그럼 이제 방도 만들었고 상대방이 방안에 들어왔다.

그런데 이때까지 내가 보낸 해시값으로는 가위바위보 게임을 할 수 없다. 그래서 내가 만약 1을 냈다면 1을 낼 수 있는 증거가 필요하다

  function useOriginator(uint256  _roomNum, uint256 _hand, string memory _secretKey) public {
    require(msg.sender == rooms[_roomNum].originator.addr);
    require(rooms[_roomNum].originator.hand == keccak(_hand,_secretKey));
    rooms[_roomNum].originator.result = _hand;
    rooms[_roomNum].originator.count = 1;
  }

    function useTaker(uint256  _roomNum, uint256 _hand, string memory _secretKey) public {
    require(msg.sender == rooms[_roomNum].taker.addr);
    require(rooms[_roomNum].taker.hand == keccak(_hand,_secretKey));
    rooms[_roomNum].taker.result = _hand;
    rooms[_roomNum].taker.count = 1;
  }


아까전에 나는 1과 cake 라는 salt 값을 썼다.

생각해보자. 해시함수는 동일한 입력값이 있다면 항상 같은 결과물을 출력한다. 그 점을 이용하는 것이다. useOriginator 나 useTaker 함수에 자신이 1을 냈으면 1을 내고 내가 salt키로 사용했던 키를 건낸다. 그리고 Player.hand 값에 저장되어 있는 해시값과 비교해서 같으면 내가 1을 냈다는 것이 증명이 된다.
만약 증명이 되지 않으면 함수는 바로 종료되지만 증명이 된다면 Player.result 에 비교할 값인 1을 넣어주게 된다.

이제 마지막으로 비교하고 이긴사람에게 돈을 지불해주면 된다.

 function _compareHands(uint256 roomNum) private {
    require(rooms[roomNum].taker.count==1 && rooms[roomNum].originator.count ==1);
    uint8 originator = uint8(rooms[roomNum].originator.result);
    uint8 taker = uint8(rooms[roomNum].taker.result);

    rooms[roomNum].gameStatus = GameStatus.STATUS_STARTED;
//result 값을 비교해 Player의 PlayerStatus 값을 바꿔준다.
    if (taker == originator) {
      rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
      rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
    } else if ((taker + 1) % 3 == originator) {
      rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_WIN;
      rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_LOSE;
    } else if ((originator + 1) % 3 == taker) {
      rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_LOSE;
      rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_WIN;
    } else {
      rooms[roomNum].gameStatus = GameStatus.STATUS_ERROR;
    }
  } 



function payout(uint256 roomNum) public payable isPlayer(roomNum, msg.sender) {
    _compareHands(roomNum);
    if (
      rooms[roomNum].originator.playerStatus == PlayerStatus.STATUS_TIE &&
      rooms[roomNum].taker.playerStatus == PlayerStatus.STATUS_TIE
    ) {
      rooms[roomNum].originator.addr.transfer(
        rooms[roomNum].originator.playerBetAmount
      );
      rooms[roomNum].taker.addr.transfer(rooms[roomNum].taker.playerBetAmount);
    } else {
      if (rooms[roomNum].originator.playerStatus == PlayerStatus.STATUS_WIN) {
        rooms[roomNum].originator.addr.transfer(rooms[roomNum].betAmount);
      } else if (rooms[roomNum].taker.playerStatus == PlayerStatus.STATUS_WIN) {
        rooms[roomNum].taker.addr.transfer(rooms[roomNum].betAmount);
      } else {
        rooms[roomNum].originator.addr.transfer(
          rooms[roomNum].originator.playerBetAmount
        );
        rooms[roomNum].taker.addr.transfer(
          rooms[roomNum].taker.playerBetAmount
        );
    }
    rooms[roomNum].gameStatus = GameStatus.STATUS_COMPLETE;
  }
 }

이제 payout 함수를 호출하는 트랜잭션을 보내면 경기 결과에 따라서 각 계정에 이더가 보내지게 된다.

회고

이번에 가위바위보 게임 개선하기를 하면서 solidity 에 대한 이해도가 조금 발전 한 것 같아서 기쁘다. 다른 사람이 짠 코드를 분석해보고 어떻게 활용할지를 더욱 더 생각해봐야겠다..

그리고 또 하나의 느낀점이라면 스마트 컨트랙트는 꼼꼼히 짜야한다는 것을 다시 한번 느꼈다. 하나의 허점이 발견된다면 컨트랙트는 무용지물이 되는 것 같다. 한번이라도 배포하게 된다면 더이상 수정을 할 수 없는 블록체인의 특성 때문에 배포하기 전에 계속 공격 받을 수 있는 상황(예외 상황) 같은 것들을 꼼꼼히 생각하고 배포해야겠다는 생각이 들었다.

컨트랙트 주소
0x08BC72C12169c525653AD8106416Dd0e6105362a

컨트랙트 링크
https://ropsten.etherscan.io/address/0x08BC72C12169c525653AD8106416Dd0e6105362a

profile
I will be Blockchain Core Developer

0개의 댓글