[Solidity] Truffle 공부 - 9(Lottery 승패 함수 개발)
Solidity 메모9!
유튜브 영상 자료(Dapp Campus)를 따라해보면서 배우고 있는 중입니다. 한번 따라해보면 처음에 익숙해지는데 꽤나 좋을 것 같습니다.
여기서는 로또비스무리한 컨트랙트를 만들고 있군요. 그래서 Lottery Contract로 만들어 보고 있습니다.
오늘은 Solidity 기능개발의 마지막입니다.
추가로 알아 갈 내용들
- blockhash(blockNumber) 함수로 블럭해시를 알수 있다.
- new web3.utils.BN("string value") => BigNumber 함수를 제공한다.
- 스마트컨트랙트로 ETH를 보내는 방법 3가지
- transfer 함수 : 이더 전송에만 사용되고, 전송 실패시 트랜잭션 자체가 fail처리됨(가장 안전함)
- send : ETH를 보내긴 하는데, boolean으로 결과를 리턴(후 처리 용도시 사용 등)
- call : ETH를 보내거나 다른 스마트컨트랙트 function 호출이 가능(주로 여기서 문제가 많이 발생 함. 외부꺼 호출하다 취약점 발생)
전체 Lottery.sol 소스
pragma solidity >=0.4.21 < 0.6.0;
contract Lottery{
struct BetInfo {
uint256 answerBlockNumber;
address payable bettor; // 코인 전송을 위해서는 변수 앞에 payable을 써줘야함
byte challenges; // ex:0xab
}
enum BlockStatus {Checkable, NotRevealed, BlockLimitPassed}
enum BettingResult {Fail, Win, Draw}
// gas 관련 정보
// gas(gasLimit와 동일한 의미) : 단위가 없는 숫자
// gasPrice(wei단위를 쓰지만 너무 적은 단위이기 때문에 gwei(10 ** 9) 단위로 많이 쓴다.)
// ETH : 1 ETH = 10 ** 18 wei
// 수수료 = gas(21000) * gasPrice(gwei 단위(1gwei == 10 ** 9wei))
// public 으로 전역변수 설정시 자동으로 getter을 만들어 준다
// smart contract 외부에서 오너 확인 가능하다고 함
address payable public owner;
uint256 private _tail;
uint256 private _head;
// _bets 변수에 베팅된 값들을 저장, 위에 선언된 _tail 값을 증가시키면서 저장하고, _head 값으로 베팅값을 검증한다
mapping (uint256 => BetInfo) private _bets;
// 베팅머니는 0.005ETH로 설정
// 1 * 10 ** 18 => 1ETH, 이므로 5 * 10 ** 15가 0.005
uint256 constant internal BET_AMOUNT = 5 * 10 ** 15;
uint256 constant internal BET_BLOCK_INTERVAL = 3; // 몇 번째 뒤의 블럭을 찾을 것인가
uint256 constant internal BLOCK_LIMIT = 256;
uint256 private _pot;
bool private mode = false; // false : use answer for teset, true : real block hash
bytes32 answerForTest;
event BET(uint256 index, address 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; // 21000GAS * gasPrice
}
// view는 smartcontract에 저장된 값에 접근 시 사용
// pure는 컨트랙 접근 없이 사용 가능함 함수
function getPot() public view returns(uint256 pot){
return _pot;
}
/**
* @dev 베팅과 정답체크를 한다.
* @param challenges 유저가 베팅하는 글자
* @return 함수가 잘 수행 되었는지 확인하는 boolean값
*/
function betAndDistribute(byte challenges) public payable returns (bool result){
bet(challenges);
distribute();
return true;
}
// Bet
// 베팅 참여(코인 전송이 필요하므로 payable 추가)
/**
* @dev 베팅을 한다. 유저는 0.005이더를 보내야 하고, 베팅용 1byte 글자를 보낸다.
* 큐에 저장된 베팅 정보는 이후 distribute 함수에서 해결된다.
* @param challenges 유저가 베팅하는 글자
* @return 함수가 잘 수행 되었는지 확인하는 boolean값
*/
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;
}
// _bets 값에다가 베팅 정보 저장
// Distribute
/**
* @dev 베팅결과값을 확인하고 팟머니를 분배
* 정답 실패:팟머니 축적, 맞춤 : 팟머니 획득, 한글자 맞춤 : 베팅금액만 반환, 정답확인 불가 : 베팅 금액만 반환
*/
function distribute() public {
// 큐에 저장된 베팅 정보가 (head)3, 4, 5, 6, 7, 8, 9, 10(tail)
// 큐 순서대로 값들을 체크하고, 2자리 모두 맞힌 경우 팟머니를 전송,
// 1자리만 맞힌 경우 건 값만 돌려줌
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 : 블럭 넘버가 정답 블럭보다 커야하고, 블럭.number < BLOCK_LIMIT(256) + AnswerBlockNumber
if(currentBlockStatus == BlockStatus.Checkable){
bytes32 answerBlockHash = getAnswerBlockHash(b.answerBlockNumber);
currentBettingResult = isMatch(b.challenges, answerBlockHash);
// if win, bettor gets pot
if(currentBettingResult == BettingResult.Win){
uint256 tmpPot = _pot; // 총 수량 임시 저장
// pot = 0, pot을 0으로 만드는 작업을 가장 먼저 하는 것이 좋다.
// 그렇지 않으면 전송되는 동안 _pot 값이 아직 0이 안된 상태라서 무슨 일이 생길지.!!
_pot = 0;
// transfer pot money
transferAmount = transferAfterPayingFee(b.bettor, tmpPot + BET_AMOUNT);
// emit win event
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 event
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 event
emit DRAW(cur, b.bettor, transferAmount, b.challenges, answerBlockHash[0], b.answerBlockNumber);
}
}
// 결과를 확인 할 수 없는 경우(체크가 불가능한상태)
// Not revealed(마이닝이 되지 않은 상황) ==> block.number <= AnswerBlockNumber
if(currentBlockStatus == BlockStatus.NotRevealed){
break;
}
// block limit passed : block.number >= AnswerBlockNumber + BLOCK_LIMIT(256)
if(currentBlockStatus == BlockStatus.BlockLimitPassed){
// 환불
transferAmount = transferAfterPayingFee(b.bettor, BET_AMOUNT);
// emit refund event
emit REFUND(cur, b.bettor, transferAmount, b.challenges, b.answerBlockNumber);
}
popBet(cur);
// check the answer
}
_head = cur;
}
function transferAfterPayingFee(address payable addr, uint256 amount) internal returns(uint256){
// uint256 fee = amount / 100;
uint256 fee = 0; // test를 위해서 0
uint256 amountWithoutFee = amount - fee;
// transfer to addr
addr.transfer(amountWithoutFee);
// transfer to owner
owner.transfer(fee);
// 스마트컨트랙트 안에서 ether를 전송하는 3가지 방법
// call, send, transfer(recommended)
// transfer : 이더 전송에만 사용됨. 전송 실패시 트랜잭션 자체가 fail처리됨(가장 안전함)
// send : 돈을 보내긴 하는데, boolean으로 결과를 리턴
// call : 이더를 보내거나 외부의 다른 스마트컨트랙트의 특정 function 호출이 가능(주로 여기서 문제가 많이 발생 함. 외부꺼 호출하다 취약점 발생)
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 라고 가정(사이즈는 1byte)
// answer(block hash) : 0xab...... ff 32byte
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; // 0xab -> 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){
// returns에 선언된 변수 명을 그대로 쓰면 함수 호출 시 그게 바로 return 된다
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
// block.number은 현재 트랜잭션이 들어갈 블럭 번호를 가져옴
b.answerBlockNumber = block.number + BET_BLOCK_INTERVAL; // 32 byte(uint256 이기 때문)
b.challenges = challenges; // byte
_bets[_tail] = b; // map에 저장하는 것은 gas 소모가 크지 않다
_tail++; // 32 byte(uint256 이기 때문)
return true;
}
function popBet(uint256 index) internal returns (bool){
// delete는 스마트컨트랙트에 데이터를 더이상 저장하지 않겠다는 의미
// delete 를 사용 할 경우 가스를 돌려 받게 됨(가스비에 유리 할 듯)
delete _bets[index];
return true;
}
}
0
0
0.000
0 comments