Block Chain Development

이더리움 기반 Dapp 개발 연습 #7 (코드)

PON_Z 2022. 2. 7. 20:39

- 솔리디티 문법 중 몇 가지 추가로 알아두어야 할 것들이 있다.

 

- storage :  함수내부에 선언된 로컬 변수가 레퍼런스 타입의 상태변수를 참조할 때 사용 (상태변수를 가리키는 포인터)

- pure :  pure가 붙은 메소드가 계정의 상태정보에 영향을 주지 않는 메소드라는 의미 (생략가능)

- external : 다른 컨트랙트나 트랜잭션을 통해서만 호출될 수 있다. f()는 동작하지 않지만 this.f()는 동작한다.
- internal : 내부적으로만 접근할 수 있음. 접근을 위해 this를 사용할 수 없다.

- 솔리디티에는 null이 없음 address(0)이 null과 같은 의미

 

pragma solidity >=0.4.22 <0.9.0; // 버전 프라그마 (해당 버전 사이에 있어야 컴파일 된다는 뜻)
//pragma solidity >=0.4.24 <=0.5.4;

contract CoinFlips {

    uint constant MAX_CASE = 2;             // 앞 뒤 두 가지 케이스
    uint constant MIN_BET = 0.01 ether;     // 최소 베팅 금액
    uint constant MAX_BET = 10 ether;       // 최대 베팅 금액
    uint constant HOUSE_FEE_PERCENT = 5;    // 수수료 퍼센트
    uint constant HOUSE_MIN_FEE = 0.005 ether;  // 최소 수수료 금액

    address payable public owner;
    uint public lockedInBets;


    // 베팅 정보를 저장하기 위한 구조체
    struct Bet {
        uint amount;    // 베팅금액(wei)
        uint8 numOfBetBit;    // 플레이어가 선택한 면의 개수
        uint placeBlockNumber;  // 플레이어가 베팅한 거래 정보가 담긴 블록 번호
        uint8 mask; // 플레이어가 선택한 동전 면
        address payable gambler;    // 플레이어의 계정주소
    }

    mapping (address => Bet) bets;

    event Reveal(address indexed gambler, uint reveal, uint amount);//1 or 2
    event Payment(address indexed beneficiary, uint amount);
    event FailedPayment(address indexed beneficiary, uint amount);

    constructor() public {
        owner = msg.sender;
    }

    // owner일때만 수행하도록 도와주는 modifier
    modifier onlyOwner {
        require (msg.sender == owner, "Only owner can call this function.");
        _;
    }

    // 이더리움 인출 메소드
    function withdrawFunds(address payable beneficiary, uint withdrawAmount) external onlyOwner {
        require (withdrawAmount + lockedInBets <= address(this).balance, "larger than balance.");
        sendFunds(beneficiary, withdrawAmount);
    }

    // 이더리움 전송 메소드
    function sendFunds(address payable beneficiary, uint amount) private {
        if (beneficiary.send(amount)) {
            emit Payment(beneficiary, amount);
        } else {
            // 실패하는 경우 대책은?
            emit FailedPayment(beneficiary, amount);
        }
    }

    // 컨트랙트 비활성화 함수
    function kill() external onlyOwner {
        require(lockedInBets == 0, "All bets should be processed before self-destruct.");
        selfdestruct(owner);
    }

    // Fallback 함수, 이더리움 받기 위함
    function() external payable {}

    // 플레이어가 화면의 베팅 버튼을 클릭했을 때 호출하는 매소드
    function placeBet(uint8 betMask) external payable { // payable로 선언해서 얼마를 배팅했는지는 전달하지 않음
        uint amount = msg.value;

        require(amount >= MIN_BET && amount <= MAX_BET, "Amount is out of range.");
        require(betMask > 0 && betMask < 256, "Mask should be 8 bit");

        // storage는 함수내부에 선언된 로컬 변수가 레퍼런스 타입의 상태변수를 참조할 때 사용 (상태변수를 가리키는 포인터)
        Bet storage bet = bets[msg.sender]; // mapping bets(address => Bet)

        // 솔리디티에는 null이 없음 address(0)이 null과 같은 의미
        require (bet.gambler == address(0), "Bet should be empty state.");

        // betMas의 bit를 세기 위함
        // 0000 0011 number of bits = 2
        // 0000 0001 number of bits = 1
        uint8 numOfBetBit = countBits(betMask);

        bet.amount = amount;
        bet.numOfBetBit = numOfBetBit;
        bet.placeBlockNumber = block.number;
        bet.mask = betMask;
        bet.gambler = msg.sender;

        // 이길시 지불할 양을 lock 해두기 위함
        uint possibleWinningAmount = getWinningAmount(amount, numOfBetBit);
        lockedInBets += possibleWinningAmount;

        require(lockedInBets < address (this).balance, "Cannot afford to pay the bet.");
    }

    // pure는 이 메소드가 계정의 상태정보에 영향을 주지 않는 메소드라는 의미 (생략가능)
    function getWinningAmount(uint amount, uint8 numOfBetBit) private pure returns (uint winningAmount) {
        require (0 < numOfBetBit && numOfBetBit < MAX_CASE, "Probability is out of range");

        uint houseFee = amount * HOUSE_FEE_PERCENT / 100;

        if(houseFee < HOUSE_MIN_FEE) {
            houseFee = HOUSE_MIN_FEE;
        }

        uint reward = amount / (MAX_CASE + (numOfBetBit-1));

        winningAmount = (amount - houseFee) + reward;
    }

    // 플레이어에게 결과를 알려주는 함수
    function revealResult() external {

        Bet storage bet = bets[msg.sender];
        uint amount = bet.amount;
        uint8 numOfBetBit = bet.numOfBetBit;
        uint placeBlockNumber = bet.placeBlockNumber;
        address payable gambler = bet.gambler;

        require (amount > 0, "Bet should be in an active state");

        // 베팅에 해당하는 블록이 결과에 해당하는 블록보다 먼저 생성되어야 하는 것을 검토하는 require
        require(block.number > placeBlockNumber, "revealResult in the same block as placeBet, or before.");

        // 난수 생성
        bytes32 random = keccak256(abi.encodePacked(blockhash(block.number), blockhash(placeBlockNumber)));
        uint reveal = uint(random) % MAX_CASE; // 0 or 1

        uint winningAmount = 0;
        uint possibleWinningAmount = 0;
        possibleWinningAmount = getWinningAmount(amount, numOfBetBit);

        if ((2 ** reveal) & bet.mask != 0) {
            winningAmount = possibleWinningAmount;
        }

        emit Reveal(gambler, 2 ** reveal, winningAmount);

        lockedInBets -= possibleWinningAmount;
        clearBet(msg.sender);

        if (winningAmount > 0) {
            sendFunds(gambler, winningAmount);
        }
    }

    // 항목들 null로 세팅하는 메소드
    function clearBet(address player) private {
        Bet storage bet = bets[player];
        // for safety
        if(bet.amount > 0) {
            return;
        }
        bet.amount = 0;
        bet.numOfBetBit = 0;
        bet.placeBlockNumber = 0;
        bet.mask = 0;
        bet.gambler = address (0);
    }

    // 결과 확인 전에 환불하는 메소드
    function refundBet() external {
        Bet storage bet = bets[msg.sender];

        uint8 numOfBetBit = bet.numOfBetBit;
        uint amount = bet.amount;
        address payable gambler = bet.gambler;

        require(block.number > bet.placeBlockNumber, "refundBet in the same block as placeBet, or before.");
        require(amount > 0, "Bet should be in an active state");

        uint possibleWinningAmount;
        possibleWinningAmount = getWinningAmount(amount, numOfBetBit);

        lockedInBets -= possibleWinningAmount;
        clearBet(msg.sender);

        sendFunds(gambler, amount);
    }

    function checkHouseFund() public view onlyOwner returns(uint) {
        return address(this).balance;
    }

    function countBits(uint8 _num) internal pure returns (uint8) {
        uint8 count;
        while(_num > 0) {
            count += _num & 1;
            _num >>= 1;
        }
        return count;
    }

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ref) https://www.inflearn.com/course/dapp/dashboard

728x90