2강 Dapp만들기

YU YU·2021년 10월 27일
0

https://www.youtube.com/playlist?list=PLlYCl1UOH8dheHS4vHOpPoHwq4Qi0R7WM
위 영상을 따라 만든 내용입니다.

2-1.Dapp 서비스 설계

  1. 지갑 관리
  2. 아키텍쳐
  3. 코드
    -코드를 실행하는데 돈이 든다.
    • 권한 관리
    • 비즈니스 로직 업데이트
    • 데이터 마이그레이션
  4. 운영
    -public
    -private

2-2. lottery Domain 및 Queue설계

-contracts > Lottery.sol을 다음과 같이 바꾸어준다.

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

contract Lottery {

  struct BetInfo{
    uint256 answerBlockNumber;//우리가 맞추려는 정답의 블록 number
    address payable better;//정답을 맞추면 better에게 보내줘야 함. 그래서 payable을 써주ㅕ야 함.
    byte challenges;
  }

  address public owner;


  uint256 private _pot;//팟머니를 만들 곳

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

  function getSomeValue() public pure returns (uint value){
    return 5;
  }

  function getPot() public view returns (uint256 pot) {
    return _pot;
  }
}

-test > lottery.test.js에 내용을 추가해준다.

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

contract Lottery {

  struct BetInfo{
    uint256 answerBlockNumber;//우리가 맞추려는 정답의 블록 number
    address payable better;//정답을 맞추면 better에게 보내줘야 함. 그래서 payable을 써주ㅕ야 함.
    byte challenges;
  }

  address public owner;


  uint256 private _pot;//팟머니를 만들 곳

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

  function getSomeValue() public pure returns (uint value){
    return 5;
  }

  function getPot() public view returns (uint256 pot) {
    return _pot;
  }
}

$ npx truffle test test/lottery.test.js
를 하면 다음과 같이 된다.

2-3.Lottery Bet 함수 구현

https://docs.soliditylang.org/en/v0.8.9/units-and-global-variables.html 여기로 들어가면 변수들을 알 수 있다.

외부에서 랜덤시드값을 넣어주기도 한다.

2-3-1. Bet함수의 역할

save the bet to the queue
queue 안에 bet 정보를 저장한다.
돈이 제대로 들어왔는지도 확인한다.
challenges: 배팅한 글자

  • test>lottery.test.js
const Lottery = artifacts.require("Lottery");

contract('Lottery',function ([deployer,user1,user2]){
    let lottery;
    beforeEach(async()=>{
        console.log('Before each');
        lottery = await Lottery.new();//배포해 줄 수 있다. 
    })
    it ('Basic test',async()=>{
        console.log('Basic test');
        let owner = await lottery.owner();
        // let value = await lottery.getSomeValue();
        console.log(`owner ${owner}`);
        // console.log(`value ${value}`);
        assert.equal(value,5);
    })
    it.only('getPot should return current pot',async()=>{
        //mochatest에서 특정 부분만 테스트할 때 only를 써주면 된다.
        let pot = await lottery.getPot();
        assert.equal(pot,0); 
    })
})

그리고 getsomevalue를 지워준다.

  • contracts>Lottery.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Lottery {

  struct BetInfo{
    uint256 answerBlockNumber;//우리가 맞추려는 정답의 블록 number
    address payable bettor;//정답을 맞추면 better에게 보내줘야 함. 그래서 payable을 써주ㅕ야 함.
    byte challenges;
  }

  uint256 constant internal BET_BLOCK_INTERVAL = 3;
  uint256 constant internal BET_AMOUNT = 5 * 10 ** 15; 

  event BET(uint256 index,address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);

  uint256 private _tail;
  uint256 private _head;
  mapping (uint256 =>BetInfo) private _bets;//queue
  address public owner;


  uint256 private _pot;//팟머니를 만들 곳

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

  // function getSomeValue() public pure returns (uint value){
  //   return 5;
  // }

  function getPot() public view returns (uint256 pot) {
    return _pot;
  }

  function getBetInfo(uint256 index)public view returns(uint256 answerBlockNumber, address bettor, byte challenges){
    BetInfo memory b = _bets[index];
    answerBlockNumber = b.answerBlockNumber;
    bettor = b.bettor;
    challenges = b.challenges;
  } 
  function pushBet(byte challenges) internal returns (bool) {
    BetInfo memory b;
    b.bettor = msg.sender;
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL;
    b.challenges = challenges;

    _bets[_tail] = b;
    _tail++;

    return true;

  }

  function popBet(uint256 index) internal returns (bool){
    delete _bets[index];
    return true;
    //딜리트를하면 가스가 환불이 된다. 상태 데이터베이스의 값을 없애겠다는 것임.
    //필요하지 않는 값에 대해서는 delete를 해주는 것이 맞다. 
  }
  //bet

  /**
  * @dev 배팅을 한다. 유저는 0.005eth를 보내야 하고 배팅용 1byte 글자를 보낸다.
  *큐에 저장된 배팅 정보는 이후 distribution 에 저장된다. 
  *@param challenges 유저가 배팅하는 글자
  *@return 함수가 잘 수행되었느지 확인하는 bool값
   */
  function bet(byte challenges) public payable returns (bool result) {
    //1. check the propter ether is sent
    require(msg.value == BET_AMOUNT,"Not enough ETH" );

    //2. push bet to the queue
    require(pushBet(challenges),"Fail to add a new Bet Info");
    //3. emit event log
    emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);
    
  }
}
//결과값을 겁ㅁ증해야 하는데 Bet 과 Distribute로 하면 될 듯
//save the bet to the queue
//distribute
//r값이 틀리면 넣고 

제대로 되었는지는 npx truffle compile을 하면 알 수 있다. compile을 할 때엔 build폴더를 지우고 하는 것이 좋다.

2-4. Lottery Bet 테스트

-test>lottery.test.js

const Lottery = artifacts.require("Lottery");

contract('Lottery',function ([deployer,user1,user2]){
    let lottery;
    beforeEach(async()=>{
        console.log('Before each');
        lottery = await Lottery.new();//배포해 줄 수 있다. 
    })
    it ('Basic test',async()=>{
        console.log('Basic test');
        let owner = await lottery.owner();
        // let value = await lottery.getSomeValue();
        console.log(`owner ${owner}`);
        // console.log(`value ${value}`);
        // assert.equal(value,5);
    })
    // it.only('getPot should return current pot',async()=>{
    //     //mochatest에서 특정 부분만 테스트할 때 only를 써주면 된다.
    //     let pot = await lottery.getPot();
    //     assert.equal(pot,0); 
    // })

    describe('Bet',function () {
        it('shold fail when the bet money is not 0.005 ETH', async () => {
            //Fail transaction
            //스마트 컨트랙트를 만들때는 transaction object이라는 것을 줄 수가 있다.
            //transaction object {chainId, value, to from, gas(Limit), gasPrice} 
            await lottery.bet('0xab',{from :user1, value:4000000000000000})
        })
        it('should put the bet to the bet queue with 1 bet', async () => {
            //bet

            //check ontract balance ==0.005

            //check bet info betinfo에 제대로 들어갔는지 확인해봐야 한다

            //check log
        })
    
    
    })
})


안된다고 뜬다.

ETH가 부족하다고 뜬다.

describe('Bet',function () {
        it('shold fail when the bet money is not 0.005 ETH', async () => {
            //Fail transaction
            //스마트 컨트랙트를 만들때는 transaction object이라는 것을 줄 수가 있다.
            //transaction object {chainId, value, to from, gas(Limit), gasPrice} 
            await lottery.bet('0xab',{from :user1, value:5000000000000000})
        })

이 부분을 describe('Bet'~)부분을 위와 같이 수정해보자. 0.005이더여서 성공할 것이다.
$ npx truffle test test/lottery.test.js


위와 같이 잘 성공하였음을 알 수 있다.

open zeppelin
제일 많이 쓰이는 오픈소스 컨트랙트 라이브러리 모음이다.

const Lottery = artifacts.require("Lottery");
const assertRevert = require('./assertRever')
contract('Lottery',function ([deployer,user1,user2]){
    let lottery;
    beforeEach(async()=>{
        console.log('Before each');
        lottery = await Lottery.new();//배포해 줄 수 있다. 
    })
    it ('Basic test',async()=>{
        console.log('Basic test');
        let owner = await lottery.owner();
        // let value = await lottery.getSomeValue();
        console.log(`owner ${owner}`);
        // console.log(`value ${value}`);
        // assert.equal(value,5);
    })
    // it.only('getPot should return current pot',async()=>{
    //     //mochatest에서 특정 부분만 테스트할 때 only를 써주면 된다.
    //     let pot = await lottery.getPot();
    //     assert.equal(pot,0); 
    // })

    describe('Bet',function () {
        it('shold fail when the bet money is not 0.005 ETH', async () => {
            //Fail transaction
            //스마트 컨트랙트를 만들때는 transaction object이라는 것을 줄 수가 있다.
            //transaction object {chainId, value, to from, gas(Limit), gasPrice} 
            await assertRevert(lottery.bet('0xab',{from :user1, value:5000000000000000}));
            //try catch 문으로 보내서 revert가 들어있으면 제대로 잡았다. 
        })
        it.only('should put the bet to the bet queue with 1 bet', async () => {
            //bet
            await lottery.bet('0xab',{from :user1, value:5000000000000000});
            //check ontract balance ==0.005

            //check bet info betinfo에 제대로 들어갔는지 확인해봐야 한다

            //check log
        })
    
    
    })
})

여기서 오류가 난 것 같았지만 그냥 넘어갔다. ...1시간 버림 ㅠㅜ

describe부분을 이렇게 바꾸면 엄청난 오류가 난다. ㅠㅠ

describe('Bet',function () {
        it('shold fail when the bet money is not 0.005 ETH', async () => {
            //Fail transaction
            //스마트 컨트랙트를 만들때는 transaction object이라는 것을 줄 수가 있다.
            //transaction object {chainId, value, to from, gas(Limit), gasPrice} 
            await assertRevert(lottery.bet('0xab',{from :user1, value:4000000000000000}));
            //try catch 문으로 보내서 revert가 들어있으면 제대로 잡았다. 
        })
        it('should put the bet to the bet queue with 1 bet', async () => {
            //bet
            await lottery.bet('0xab',{from :user1, value:5000000000000000});
            //check ontract balance ==0.005
            //check bet info betinfo에 제대로 들어갔는지 확인해봐야 한다
            //check log
        })



고쳤다.... 이 동영상 하는 사람이 너무 빨리가서 나는 입력을 안해도 됐다고 생각한 걸 입력을 해주었어야 했다.
-test> assertRever.js

module.exports = async (promise) => {
    try {
        await promise;
        assert.fail('Expected revert not received');
    } catch (error) {
        const revertFound = error.message.search('revert') >= 0;
        assert(revertFound, `Expected "revert", got ${error} instead`);
    }
}

require assert 차이
https://steemit.com/kr/@ryugihyeok/kr-revert-assert-require
require은 로직에 맞지 않으면 바로 로직을 끝내는 반면, assert는 조건에 맞지 않아도 끝까지 함수를 실행시킨다. 그래서 assert는 변경후 상태를 확인하거나 절대 불가능한 로직을 검사하거나 오버플로우, 언더플로우 검사할 때 사용한다고 한다.

-test> lottery.test.js
receipt라는 변수에 넣어서 console.log로 찍어보자.

   it('should put the bet to the bet queue with 1 bet', async () => {
            //bet
            let receipt = await lottery.bet('0xab',{from :user1, value:5000000000000000});
            console.log(receipt);
            //check ontract balance ==0.005

            //check bet info betinfo에 제대로 들어갔는지 확인해봐야 한다

            //check log
        })


여기서 보면 tx hash값이 나온 것을 확인할 수 있고, gasUsed를 통해 얼마나 가스가 소모되었는지, 그리고 logs를 살펴보면 어떤 이벤트가 실행되었는지도 알 수 있다.

최종코드

  • test >lottery.test.js
const Lottery = artifacts.require("Lottery");
const assertRevert = require('./assertRever');
const expectEvent = require('./expectEvent')


contract('Lottery',function ([deployer,user1,user2]){
    let lottery;
    let betAmount = 5 * 10 ** 15;
    let bet_block_interval = 3;
    beforeEach(async()=>{
        console.log('Before each');
        lottery = await Lottery.new();//배포해 줄 수 있다. 
    })
    it ('Basic test',async()=>{
        console.log('Basic test');
        let owner = await lottery.owner();
        // let value = await lottery.getSomeValue();
        console.log(`owner ${owner}`);
        // console.log(`value ${value}`);
        // assert.equal(value,5);
    })
    // it.only('getPot should return current pot',async()=>{
    //     //mochatest에서 특정 부분만 테스트할 때 only를 써주면 된다.
    //     let pot = await lottery.getPot();
    //     assert.equal(pot,0); 
    // })

    describe.only('Bet',function () {
        it('shold fail when the bet money is not 0.005 ETH', async () => {
            //Fail transaction
            //스마트 컨트랙트를 만들때는 transaction object이라는 것을 줄 수가 있다.
            //transaction object {chainId, value, to from, gas(Limit), gasPrice} 
            await lottery.bet('0xab',{from :user1, value:4000000000000000});
            //try catch 문으로 보내서 revert가 들어있으면 제대로 잡았다. 
        })
        it('should put the bet to the bet queue with 1 bet', async () => {
            //bet
            let receipt = await lottery.bet('0xab',{from :user1, value:betAmount});
            console.log(receipt);
            
            let pot=  await lottery.getPot();
            assert.equal(pot,0);
            
            //check contract balance ==0.005
            let contractBalance = await web3.eth.getBalance(lottery.address);
            assert.equal(contractBalance,betAmount);

            //check bet info betinfo에 제대로 들어갔는지 확인해봐야 한다
            let currentBlockNumber = await web3.eth.getBlockNumber();//현재 마이닝된 블럭 넘버를 가져온다. 
            let bet = await lottery.getBetInfo(0);
            assert.equal(bet.answerBlockNumber,currentBlockNumber + bet_block_interval);
            assert.equal(bet.bettor,user1);
            assert.equal(bet.challenges,'0xab');
            //check log
            // console.log(receipt.logs
        await expectEvent.inLogs(receipt.logs,'BET');        
        })
    
    
    })
})
  • test>assertRever.js
module.export = async(promise)=>{
    try{
        await promise;
        assert.fail('Expectee rever not recedived');
    }catch(error){
        const revertFound = error.message.search('revert') >= 0;
        assert(revertFound, `Expected "revert", got ${error} instead`);
    }
}
  • test >expectEvent.js
const assert= require('chai').assert;
//npm i chai

const inLogs = async (logs,eventName)=>{
    const event = logs.find(e=>e.event === eventName);
    assert.exists(event);
}
module.exports = {
    inLogs
}

2-5. calculate Etherume GAS

2-5-1.수수료

  • gas
    gasLimit과 같은 말
  • gasPrice
    -ETH
  • 수수료 = gas* gasPrice

2-5-2. 함수당 소모 GAS량

  • 32bytes 새로 저장하면 20,000gas 소모> 무한정 저장할 수 없다.
  • 32 bytes 기존 변수에 있는 값 바꿀 때 5000 gas 소모
  • 기존 변수를 초기화해서 더 쓰지 않을 때 10,000 gas return

우리가 사용했던 bet function 같은 경우 처음 실행했을 때는 90846 gas가 들지만 두번재부터는 그보다 줄어든 75846 gas가 들게 된다.

transaction을 일으키는데는 기본으로 21,000 gas가 소모된다. 또한 event 값을 일으키는데도 375 gas가 소모되고, 그 안에 변수값을 하는데도 하나하나 375 gas가 소모된다.

2-6. Distribute function 설계

2-6-1. Distribute 함수의 역할

정답을 체크하고 정답을 맞춘 사람에게 돈을 돌려주고 정답 못 맞춘 사람에게는 돈을 hotmoney에 저장한다. ㅣ

2-6-2. getBlockStatus()

이 함수는 블록의 넘버를 통해 BlockStatus를 반환해주는 함수이다.
1. 만일 지금 들어온 블록이 '우리가 찾는 블록의 넘버'보다 크고, '우리가 찾는 블록의 넘버'에 256을 더한 것보다 적을 때 해시값을 찾을 수 있기때문에 BlockStatus.Checkable을 반환해준다.
2. 만일 지금 블럭의 번호가 우리가 찾는 블록의 번호보다 작거나 같다면 아직 더 마이닝이 필요한 단계이다.
3. 만일 지금의 블록의 번호가 '우리가 찾는 블록의 넘버'에 한계값인 245을 더한 것보다 클 때는 수를비교할 수 없기에 반환을 해주어야 한다. BlockStatus.BlockLimitPassed를 반환한다.

enum BlockStatus {Checkable, NotRevealed, BlockLimitPassed};

function getBlockStatus(uint answerBlockNumber) internal view returns (BlockStatus){
    if(block.number > answerBlockNumber && block.number < BLOCK_LIMIT +  answerBlockNumber) {
      return BlockStatus.Checkable;
    }
    if(block.number<=answerBlockNumber){
      return BlockStatus.NotRevealed;
    }
    if(block.number >= answerBlockNumber + BLOCK_LIMIT) {
      return BlockStatus.BlockLimitPassed;
    }
    return BlockStatus.BlockLimitPassed;
  }

2-6-3. distribute()

_bets의 내용을 for문을 통해 돌려줌으로써 각각의 원소들의 상태를 통해 경우의 수를 나눈다.

function distribute() public {
    uint256 cur;
    BetInfo memory b;
    BlockStatus currentBlockStatus;
    for(cur=_head; cur<_tail; cur++){
        b = _bets[cur];
        currentBlockStatus = getBlockStatus(b.answerBlockNumber);
      //Checkable: block.number > AnswerBlockNumber && block.number < BLOCK_LIMIT + AnswerBlockNumber 1
      if(currentBlockStatus == BlockStatus.Checkable){
        //if win, bettor gets pot

        //if fail, bettor's money goes pot

        //if draw, refund bettor's money

      //Not Revealed: block.number <= AnswerBlockNumber 2
      if(currentBlockStatus == BlockStatus.NotRevealed) {
        break;//아직 마이닝이 되지 않았다.
      }

      //Block Limit Passed : block.number >= AnswerBlockNumber+ Block_LiMIT 3
      if(currentBlockStatus == BlockStatus.BlockLimitPassed){
        //refund
        //emit refund
      }

      }

      //Not Revealed

      //Block Limit Passed
    }
  }

enum 배열


2-7. Lottery isMatch함수 구현 및 테스트

위에서 값을 비교할 수 있을 때 3가지 경우의 수가 나올 수 있다는 것을 알았다.

  • 값을 정확히 맞춘 경우

  • 값을 맞추는 것을 모두 실패했을 경우

  • 값을 하나만 맞춘 경우

    그것을 이제 isMatch함수를 이용해서 나누어보도록 하겠다.

2-7-1. Lottery isMatch 함수구현

-contracts>lottery.sol에서 다음 부분을 추가한다.

  enum BettingResult { Fail, Win,Draw }

  /**
  *@dev 배팅 글자와 정답을 확인한다.
  *@param challenges 배팅 글자
  *@param answer 블록해시
  *@return 정답 결과
  */



  function isMatch(byte challenges, bytes32 answer) public pure returns (BettingResult) {
    //challenges  에는 0xab이렇게 들어올 것이다.ㅣ 
    //answer 0xab...........ff 32byte로 들어올 것이다.ㅣ 
    byte c1 = challenges;
    byte c2 = challenges;

    byte a1 = answer[0];
    byte a2 = answer[0];

    //Get first number
    c1 = c1 >> 4; 
    c1 = c1 << 4;

    //0xab=>0x0a=>0xa0

    a1 = a1 >> 4;
    a1 = a1 << 4;
    
    //Get Second Number
    c2 = c2 << 4;
    c2 = c2 >> 4;

    a2 = a2 << 4;
    a2 = a2 >> 4;
    //0xab => 0xb0 =>0x0b;
  
    if(a1 == c1 && a2 == c2) {
      return BettingResult.Win;
    }
    if(a1 == c1 || a2 == c2) {
      return BettingResult.Draw;
    }
    return BettingResult.Fail;

  
  }

enum 배열이기에 실제로는 0,1,2 이렇게 반환이 된다.

shift
2진수의 shift와 16진수의 shift
Shift 연산은 2진수를 기준으로 생각을 하면 쉽다.
0b00000001 를 1만큼 좌 시프트 시키면.. 0b00000010 이 된다.
0b00000001 를 2만큼 좌 시프트 시키면.. 0b00000100 이 된다.
0b00000001 를 4만큼 좌 시프트 시키면.. 0b00010000 이 된다.
그럼 16진수로 넘어와서..
0x00000001 를 1만큼 시프트 시키면.. 0x00000002 이 된다.
0x00000001 를 3만큼 시프트 시키면.. 0x00000008 이 된다.
0x00000001 를 4만큼 시프트 시키면.. 0x00000010 이 된다.
0x00000001 를 0x00000010 로 만드려면 4 만큼 시프트
0x00000001 를 0x00000100 로 만드려면 8 만큼 시프트
0x00000001 를 0x00001000 로 만드려면 12 만큼 시프트
간단한 원리지만.. 16진수를 기준으로 자릿수 한 개를 Shift 할때는 (4*자릿수)만큼 Shift 해야한다는소리다!

컴파일을 진행한다.
npx truffle compile

2-7-2. 테스트

먼저 해시값으로 넣을 값을 찾아보겠다. 터미널창에 다음과 같이 입력해준다.
$ web3.eth.getBlockCount()

$ web3.eth.getBlock(372)

위와 같이 하려면 $ truffle console을 치면 된다.

  • test > lottery.test.js
    다음과 같은 코드를 추가해준다.
describe.only('isMatch',function(){
        let blockHash = "0xd3b8938e583c758a102be9b53419707f3258044a01c15be194a4a74690eedf82";
        it('should be BettingResult',async () =>{           
            let matchingResult = await lottery.isMatch('0xd3',blockHash);
            assert.equal(matchingResult,1);
        })

        it('should be BettingResult.Fail',async()=>{
            let matchingResult = await lottery.isMatch('0xf2',blockHash);
            assert.equal(matchingResult,0);
        })
        it('shold be BettingResult.Draw',async()=>{
            let matchingResult = await lottery.isMatch('0xd2',blockHash);
            assert.equal(matchingResult,2);
        })
    })

2-8. distribute 함수 구현

2-8-1. 배포용 모드, 테스트용 모드 설정

우리가 blockhash를 이용해서 테스트를 해봐야 하는데 랜덤으로 주어지는 값은 테스트하기가 무척 어렵다. 그래서 배포용 모드와 테스트용 모드를 나눠서 개발한다. ㅣ

2-8-2. eth를 전송하는 방법

call

  • 다른 스마트 컨트랙트 함수를 호출할 수도 있다.
  • 보안에 심각한 문제가 될 수도 있다.
  • 스마트 컨트랙트를 호출하는게 아니라면 사용하지 않는 것이 좋다.

send

  • 돈을 보내도 false를 return 함

transfer

  • 돈을 보내고 안되면 transaction을 fail시켜버림.
  • 가장 많이 쓴다. 가장 안전함.

transfer이 아닌 send나 call을 사용해 eth를 전송하려면 먼저 보내기 전에 상태값에서 값을 빼주는게 좋다.

2-8-3. 여태까지의 코드

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

contract Lottery {

  struct BetInfo{
    uint256 answerBlockNumber;//우리가 맞추려는 정답의 블록 number
    address payable bettor;//정답을 맞추면 better에게 보내줘야 함. 그래서 payable을 써주ㅕ야 함.
    byte challenges;
  }
  uint256 constant internal BLOCK_LIMIT = 256;
  uint256 constant internal BET_BLOCK_INTERVAL = 3;
  uint256 constant internal BET_AMOUNT = 5 * 10 ** 15; 

  enum BlockStatus {Checkable, NotRevealed, BlockLimitPassed}

  uint256 private _tail;
  uint256 private _head;
  mapping (uint256 => BetInfo) private _bets;//queue
  address payable public owner;

  

  uint256 private _pot;//팟머니를 만들 곳

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

  // function getSomeValue() public pure returns (uint value){
  //   return 5;
  // }
  event BET(uint256 index,address indexed bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);
  event WIN(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
  event FAIL(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
  event DRAW(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
  event REFUND(uint256 index, address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber); 
  //정답을 알 수 없으니까
  

  function getPot() public view returns (uint256 pot) {
    return _pot;
  }

  function getBetInfo(uint256 index)public view returns (uint256 answerBlockNumber, address bettor, byte challenges) {
    BetInfo memory b = _bets[index];
    answerBlockNumber = b.answerBlockNumber;
    bettor = b.bettor;
    challenges = b.challenges;
  } 
  function pushBet(byte challenges) internal returns (bool) {
    BetInfo memory b;
    b.bettor = msg.sender;
    b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL;
    b.challenges = challenges;

    _bets[_tail] = b;
    _tail++;

    return true;

  }

  function popBet(uint256 index) internal returns (bool){
    delete _bets[index];
    return true;
    //딜리트를하면 가스가 환불이 된다. 상태 데이터베이스의 값을 없애겠다는 것임.
    //필요하지 않는 값에 대해서는 delete를 해주는 것이 맞다. 
  }
  //bet

  /**
  * @dev 배팅을 한다. 유저는 0.005eth를 보내야 하고 배팅용 1byte 글자를 보낸다.
  *큐에 저장된 배팅 정보는 이후 distribution 에 저장된다. 
  *@param challenges 유저가 배팅하는 글자
  *@return 함수가 잘 수행되었느지 확인하는 bool값
   */
  function bet(byte challenges) public payable returns (bool result) {
    //1. check the propter ether is sent
    require(msg.value == BET_AMOUNT,"Not enough ETH" );

    //2. push bet to the queue
    require(pushBet(challenges),"Fail to add a new Bet Info");
    //3. emit event log
    emit BET(_tail - 1, msg.sender, msg.value , challenges, block.number + BET_BLOCK_INTERVAL);
    return true;
  }
  bool private mode = false;// false:devmod
  bytes32 public answerForTest;
  //정답을 지정할 수 있게 setterFunction 도 만들어준다.ㅣ 
  function setAnswerForTest(bytes32 answer) public returns (bool result){
    answerForTest = answer;
    return true;
  }

  function getAnswerBlockHash(uint256 answerBlockNumber) internal view returns (bytes32 answer){
    require(msg.sender ==owner, "Only owner can set the answer ");
    return mode ?  blockhash(answerBlockNumber): answerForTest;
  } 



  /**
  *@dev 배팅 결과값을 확인하고 팟머니를 분배한다. 
  *정답실패: 팟머니 축적, 정답 맞춤: 팟머니 획득, 한글자 맞춤 or 정답 확인 불가: 배팅 금액만 획득
  */
   function distribute() public {
    uint256 cur;
    uint256 transferAmount; // 이유: 얼마나 보냈는지 찍기 위해서

    BetInfo memory b;
    BlockStatus currentBlockStatus;
    BettingResult currentBettingResult;
    for(cur=_head; cur<_tail; cur++){
        b = _bets[cur];
        currentBlockStatus = getBlockStatus(b.answerBlockNumber);
      //Checkable: block.number > AnswerBlockNumber && block.number < BLOCK_LIMIT + AnswerBlockNumber 1
        if(currentBlockStatus == BlockStatus.Checkable){
          bytes32 answerBlockHash = getAnswerBlockHash(b.answerBlockNumber);
          currentBettingResult = isMatch(b.challenges,answerBlockHash);//결과값을 가져옴
        //if win, bettor gets pot
          if(currentBettingResult == BettingResult.Win){
              //transfet pot/수수료를 떼가는 함수 만들자 trnasferAfterPaingFee
              transferAfterPayingFee(b.bettor, _pot + BET_AMOUNT);//아직 내가 배팅한 금액은 추가되지 않았기 때문에
              //pot = 9
              _pot = 0;
              //transfer여서 이렇게 쓰는 것임

              //emit Win
              emit WIN(cur, b.bettor, transferAmount, b.challenges,answerBlockHash[0], b.answerBlockNumber);
          }
        //if fail, bettor's money goes pot
          if(currentBettingResult == BettingResult.Fail){
              //pot = pot + BET_AMOUNT
              _pot += BET_AMOUNT;
              //emit FAIL
              emit FAIL(cur, b.bettor, 0, b.challenges,answerBlockHash[0], b.answerBlockNumber);
          }
        //if draw, refund bettor's money
          if(currentBettingResult == BettingResult.Draw){
              //transfer only BET_AMOUNT
              transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);
              
              //emit DRAW
              emit DRAW(cur, b.bettor, transferAmount, b.challenges,answerBlockHash[0], b.answerBlockNumber);
          }
        //Not Revealed: block.number <= AnswerBlockNumber 2
        if(currentBlockStatus == BlockStatus.NotRevealed) {
          break;//아직 마이닝이 되지 않았다.
        }

        //Block Limit Passed : block.number >= AnswerBlockNumber+ Block_LiMIT 3
        if(currentBlockStatus == BlockStatus.BlockLimitPassed){
          //refund
          transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);

          //emit refund
          emit REFUND(cur, b.bettor, transferAmount, b.challenges, b.answerBlockNumber);
        }
        popBet(cur);

      }
      _head = cur;

      //Not Revealed

      //Block Limit Passed
    }
  }
  function transferAfterPayingFee(address payable addr, uint256 amount) internal returns (uint256){
    // uint256 fee = amount /100; 
    uint256 fee = 0;
    uint256 amountWithoutFee = amount -fee;
    //transfer to addr
    addr.transfer(amountWithoutFee);
    //transfer to owner
    owner.transfer(fee);
    return amountWithoutFee;
  
  }
  function getBlockStatus(uint answerBlockNumber) internal view returns (BlockStatus){
    if(block.number > answerBlockNumber && block.number < BLOCK_LIMIT +  answerBlockNumber) {
      return BlockStatus.Checkable;
    }
    if(block.number<=answerBlockNumber){
      return BlockStatus.NotRevealed;
    }
    if(block.number >= answerBlockNumber + BLOCK_LIMIT) {
      return BlockStatus.BlockLimitPassed;
    }
    return BlockStatus.BlockLimitPassed;
  }
  enum BettingResult { Fail, Win,Draw }

  /**
  *@dev 배팅 글자와 정답을 확인한다.
  *@param challenges 배팅 글자
  *@param answer 블록해시
  *@return 정답 결과
  */



  function isMatch(byte challenges, bytes32 answer) public pure returns (BettingResult) {
    //challenges  에는 0xab이렇게 들어올 것이다.ㅣ 
    //answer 0xab...........ff 32byte로 들어올 것이다.ㅣ 
    byte c1 = challenges;
    byte c2 = challenges;

    byte a1 = answer[0];
    byte a2 = answer[0];

    //Get first number
    c1 = c1 >> 4; 
    c1 = c1 << 4;

    //0xab=>0x0a=>0xa0

    a1 = a1 >> 4;
    a1 = a1 << 4;
    
    //Get Second Number
    c2 = c2 << 4;
    c2 = c2 >> 4;

    a2 = a2 << 4;
    a2 = a2 >> 4;
    //0xab => 0xb0 =>0x0b;
  
    if(a1 == c1 && a2 == c2) {
      return BettingResult.Win;
    }
    if(a1 == c1 || a2 == c2) {
      return BettingResult.Draw;
    }
    return BettingResult.Fail;

  
  }
}
 

와.... 진짜 힘들었다. 그러나 아직 하나가 더 남아있다. ㅠㅠ
고친게 너무 많아서 그냥 다 올린다.

$ npx truffle compile


컴파일이 제대로 된다...!

2-9. Testing distribute()

mocha test
https://jeonghwan-kim.github.io/mocha/

evm_mine이라는 함수로 truffle에서 마이닝을 할 수 있다.

2-9-1.contracts>Lottery.sol

내 코드가 오류가 나서 그냥 깃허브에 있는 코드를 올리도록 하겠다...
오류를 꼭 찾아봐야 것다.

pragma solidity ^0.6.0;


contract Lottery {
    struct BetInfo {
        uint256 answerBlockNumber;
        address payable bettor;
        byte challenges;
    }
    
    uint256 private _tail;
    uint256 private _head;
    mapping (uint256 => BetInfo) private _bets;

    address payable public owner;
    
    
    uint256 private _pot;
    bool private mode = false; // false : use answer for test , true : use real block hash
    bytes32 public answerForTest;

    uint256 constant internal BLOCK_LIMIT = 256;
    uint256 constant internal BET_BLOCK_INTERVAL = 3;
    uint256 constant internal BET_AMOUNT = 5 * 10 ** 15;

    enum BlockStatus {Checkable, NotRevealed, BlockLimitPassed}
    enum BettingResult {Fail, Win, Draw}

    event BET(uint256 index, address indexed bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);
    event WIN(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
    event FAIL(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
    event DRAW(uint256 index, address bettor, uint256 amount, byte challenges, byte answer, uint256 answerBlockNumber);
    event REFUND(uint256 index, address bettor, uint256 amount, byte challenges, uint256 answerBlockNumber);

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

    function getPot() public view returns (uint256 pot) {
        return _pot;
    }

    /**
     * @dev 베팅과 정답 체크를 한다. 유저는 0.005 ETH를 보내야 하고, 베팅용 1 byte 글자를 보낸다.
     * 큐에 저장된 베팅 정보는 이후 distribute 함수에서 해결된다.
     * @param challenges 유저가 베팅하는 글자

     */
    function betAndDistribute(byte challenges) public payable returns (bool result) {
        bet(challenges);

        distribute();

        return true;
    }

    // 90846 -> 75846
    /**
     * @dev 베팅을 한다. 유저는 0.005 ETH를 보내야 하고, 베팅용 1 byte 글자를 보낸다.
     * 큐에 저장된 베팅 정보는 이후 distribute 함수에서 해결된다.
     * @param challenges 유저가 베팅하는 글자
     */
    function bet(byte challenges) public payable returns (bool result) {
        // Check the proper ether is sent
        require(msg.value == BET_AMOUNT, "Not enough ETH");

        // Push bet to the queue
        require(pushBet(challenges), "Fail to add a new Bet Info");

        // Emit event
        emit BET(_tail - 1, msg.sender, msg.value, challenges, block.number + BET_BLOCK_INTERVAL);

        return true;
    }

    /**
     * @dev 베팅 결과값을 확인 하고 팟머니를 분배한다.
     * 정답 실패 : 팟머니 축척, 정답 맞춤 : 팟머니 획득, 한글자 맞춤 or 정답 확인 불가 : 베팅 금액만 획득
     */
    function distribute() public {
        // head 3 4 5 6 7 8 9 10 11 12 tail
        uint256 cur;
        uint256 transferAmount;

        BetInfo memory b;
        BlockStatus currentBlockStatus;
        BettingResult currentBettingResult;

        for(cur=_head;cur<_tail;cur++) {
            b = _bets[cur];
            currentBlockStatus = getBlockStatus(b.answerBlockNumber);
            // Checkable : block.number > AnswerBlockNumber && block.number  <  BLOCK_LIMIT + AnswerBlockNumber 1
            if(currentBlockStatus == BlockStatus.Checkable) {
                bytes32 answerBlockHash = getAnswerBlockHash(b.answerBlockNumber);
                currentBettingResult = isMatch(b.challenges, answerBlockHash);
                // if win, bettor gets pot
                if(currentBettingResult == BettingResult.Win) {
                    // transfer pot
                    transferAmount = transferAfterPayingFee(b.bettor, _pot + BET_AMOUNT);
                    
                    // pot = 0
                    _pot = 0;

                    // emit WIN
                    emit WIN(cur, b.bettor, transferAmount, b.challenges, answerBlockHash[0], b.answerBlockNumber);
                }
                // if fail, bettor's money goes pot
                if(currentBettingResult == BettingResult.Fail) {
                    // pot = pot + BET_AMOUNT
                    _pot += BET_AMOUNT;
                    // emit FAIL
                    emit FAIL(cur, b.bettor, 0, b.challenges, answerBlockHash[0], b.answerBlockNumber);
                }
                
                // if draw, refund bettor's money 
                if(currentBettingResult == BettingResult.Draw) {
                    // transfer only BET_AMOUNT
                    transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);

                    // emit DRAW
                    emit DRAW(cur, b.bettor, transferAmount, b.challenges, answerBlockHash[0], b.answerBlockNumber);
                }
            }

            // Not Revealed : block.number <= AnswerBlockNumber 2
            if(currentBlockStatus == BlockStatus.NotRevealed) {
                break;
            }

            // Block Limit Passed : block.number >= AnswerBlockNumber + BLOCK_LIMIT 3
            if(currentBlockStatus == BlockStatus.BlockLimitPassed) {
                // refund
                transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);
                // emit refund
                emit REFUND(cur, b.bettor, transferAmount, b.challenges, b.answerBlockNumber);
            }

            popBet(cur);
        }
        _head = cur;
    }

    function transferAfterPayingFee(address payable addr, uint256 amount) internal returns (uint256) {
        
        // uint256 fee = amount / 100;
        uint256 fee = 0;
        uint256 amountWithoutFee = amount - fee;

        // transfer to addr
        addr.transfer(amountWithoutFee);

        // transfer to owner
        owner.transfer(fee);

        return amountWithoutFee;
    }

    function setAnswerForTest(bytes32 answer) public returns (bool result) {
        require(msg.sender == owner, "Only owner can set the answer for test mode");
        answerForTest = answer;
        return true;
    }

    function getAnswerBlockHash(uint256 answerBlockNumber) internal view returns (bytes32 answer) {
        return mode ? blockhash(answerBlockNumber) : answerForTest;
    }

    /**
     * @dev 베팅글자와 정답을 확인한다.
     * @param challenges 베팅 글자
     * @param answer 블락해쉬
     * @return 정답결과
     */
    function isMatch(byte challenges, bytes32 answer) public pure returns (BettingResult) {
        // challenges 0xab
        // answer 0xab......ff 32 bytes

        byte c1 = challenges;
        byte c2 = challenges;

        byte a1 = answer[0];
        byte a2 = answer[0];

        // Get first number
        c1 = c1 >> 4; // 0xab -> 0x0a
        c1 = c1 << 4; // 0x0a -> 0xa0

        a1 = a1 >> 4;
        a1 = a1 << 4;

        // Get Second number
        c2 = c2 << 4; // 0xab -> 0xb0
        c2 = c2 >> 4; // 0xb0 -> 0x0b

        a2 = a2 << 4;
        a2 = a2 >> 4;

        if(a1 == c1 && a2 == c2) {
            return BettingResult.Win;
        }

        if(a1 == c1 || a2 == c2) {
            return BettingResult.Draw;
        }

        return BettingResult.Fail;

    }

    function getBlockStatus(uint256 answerBlockNumber) internal view returns (BlockStatus) {
        if(block.number > answerBlockNumber && block.number  <  BLOCK_LIMIT + answerBlockNumber) {
            return BlockStatus.Checkable;
        }

        if(block.number <= answerBlockNumber) {
            return BlockStatus.NotRevealed;
        }

        if(block.number >= answerBlockNumber + BLOCK_LIMIT) {
            return BlockStatus.BlockLimitPassed;
        }

        return BlockStatus.BlockLimitPassed;
    }
    

    function getBetInfo(uint256 index) public view returns (uint256 answerBlockNumber, address bettor, byte challenges) {
        BetInfo memory b = _bets[index];
        answerBlockNumber = b.answerBlockNumber;
        bettor = b.bettor;
        challenges = b.challenges;
    }

    function pushBet(byte challenges) internal returns (bool) {
        BetInfo memory b;
        b.bettor = msg.sender; // 20 byte
        b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // 32byte  20000 gas
        b.challenges = challenges; // byte // 20000 gas

        _bets[_tail] = b;
        _tail++; // 32byte 값 변화 // 20000 gas -> 5000 gas

        return true;
    }

    function popBet(uint256 index) internal returns (bool) {
        delete _bets[index];
        return true;
    }
}

2-9-2.test>lottery.test.js

describe('Distribute',function () {
        describe('When the answer is checkable', function () {
            it.only('should give the user the pot when the answer matches',async ()=>{
                await lottery.setAnswerForTest('0xd3b8938e583c758a102be9b53419707f3258044a01c15be194a4a74690eedf82',{from:deployer});
                await lottery.betAndDistribute('0xef',{from:user2,value:betAmount});
                await lottery.betAndDistribute('0xef',{from:user2,value:betAmount});
                await lottery.betAndDistribute('0xd3',{from:user1,value:betAmount});
                await lottery.betAndDistribute('0xef',{from:user2,value:betAmount});
                await lottery.betAndDistribute('0xef',{from:user2,value:betAmount});
                await lottery.betAndDistribute('0xef',{from:user2,value:betAmount});
                
                let potBefore = await lottery.getPot();//0.01ETH
                // console.log(`value :`,potBefore.toString())
                let user1BalanceBefore = await web3.eth.getBalance(user1);
                
                await lottery.betAndDistribute('0xef',{from:user2,value:betAmount});//이 때 user1에게 pot이 간다.ㅣ

                let potAfter = await lottery.getPot();//0
                let user1BalanceAfter = await web3.eth.getBalance(user1);

                //pot의 변화량 확인
                assert.equal(potBefore.toString(), new web3.utils.BN('10000000000000000').toString());
                assert.equal(potAfter.toString(), new web3.utils.BN('0').toString());

                user1BalanceBefore = new web3.utils.BN(user1BalanceBefore);
                assert.equal(user1BalanceBefore.add(potBefore).add(betAmountBN).toString(), new web3.utils.BN(user1BalanceAfter).toString());
            } )
        })
        describe('When the answer is not revealed(Not Minded',function (){

        })
        describe('When the answer is not revealed(Block limit is passed',function (){

        })
    })
profile
코딩 재밌어요!

0개의 댓글