Solidity v0.8.0 기반의 복권 컨트랙트 개발
출처: 이더리움 블록체인 게임 개발 (키더아이어, 크리스대넌) 기존에 오래된 버전의 복권 스마트 컨트랙트를 최신 버전으로 업그레이드하면서 0.8.0 버전에 맞게 학습도 하고 코드도 재구성 해봤습니다. 출처의 책을 읽으며 공부하시는 분들 중 최신 버전의 솔리디티로 학습하고 계신 분께 도움이 되면 좋겠습니다! (코드 이해를 돕기 위해 주석도 꼼꼼히 달아놨습니다. 😋)
복권
복권은 이더리움 사용의 훌륭한 사례다.
복권 역시 피라미드처럼 이더리움 블록체인 컨트랙트가 활용된 초기 사례다.
결과는 공평하며, 중앙 운영자가 상금을 삭감할 걱정이 없고, 하나의 법렵에 귀속되지 않은 채 운영 가능하다.
장래의 복권은 블록체인상에서 이루어질 가능성이 높다.
난수 생성기(RNG)
- 엔트로피 소스로 활용할 수 있는 주요 수단은 블록 해시와 외부 오라클이다.
- 복잡성과 외부 의존성을 최소화하기 위해 난수에 블록 해시를 적용할 것이다.
- 현재 블록이 아닌 이전 블록 해시만 사용할 수 있는데, 이전 블록 해시는 트랜잭션이 실행되는 시점에 알려지기 때문에 최종 난수 결과값을 예측할 수 없게하는 추가 조치가 필요하다.
- 구체적으로, 티켓 구입 기간과 당첨자 추첨 사이에 시간 지연을 설정함으로써 복권 티켓이 배포되는 시점에 당첨자를 결정하는 데 사용된 블록 해시를 알 수 없게 만들 수 있다.
만들어볼 컨트랙트
- SimpleLottrey
- 간단한 복권은 반복적이지 않고, 난수에 블록 해시를 사용하며 단 한 명의 승자만 존재한다.
- RecurringLottery
- 사용성을 포기하고, 단순성에 집중한다.
- 메인넷에 배포 가능한 현실적인 복권 컨트랙트이다.
- 새로운 복권은 여러 라운드에 걸쳐 일어나므로 추첨이 종료될 때마다 새로운 상금 풀이 시작된다.
- 사용자가 단일 티켓을 구입하는 것이 아니라 복수로 티켓을 구매할 수 있다.
- 몇 가지 향상된 보안 기능이 존재한다.
- RNGLottery
- 블록 해시를 RNG의 엔트로피 소스로 사용하는 데는 이론적으로 한계가 있다.
- 추첨을 위한 이더 상금이 블록 보상을 크게 초과하면, 채굴자들은 블록 해시를 조작해 자신이 상금을 받을 때까지 유효한 블록 해시를 버리게 만들 수 있다.
- 이 컨트랙트는 단품 구매만 가능한 일회성 복권 판매가 될 것이다.
난수 복권의 개념은, 확증적인 순서를 사용해 검증 가능한 난수를 생성하는 것이다. 모든 티켓 구매자는 티켓을 구입할 때 약정 해시를 제출한다. 이 약정 해시는 사용자의 주소와, 사용자만 아는 비밀번호의 조합을 해시함으로써 생성된다. - 발권 기간이 끝나면 각 구매자가 약정 해시 생성에 사용된 비밀번호를 공개해야 하는 기간(공개 기간)이 시작된다.
- 제출된 비밀번호는 블록체인상에서 제출자와 주소와 함께 해시되며, 이 해시가 처음에 티켓 구매 시에 사용자가 제출한 약정 해시와 일치하는지 확인하는 절차가 이어진다.
- 공개 기간 동안 비밀번호를 공개하지 않은 사용자는 당첨자 추첨에서 제외된다.
- 사용자들이 제출한 모든 비밀번호는 함께 해시돼 당첨자를 뽑을 수 있는 임의 시드를 생성한다.
- PowerBall
- 사용자는 티켓당 6개의 숫자를 선태한다.
- 처음 5개의 숫자는 1 ~ 69까지의 표순 숫자이며
여섯 번째 숫자는 1 ~ 26의 특별한 파워볼 숫자로 추가 보상을 제공한다. - 매 3~4일마다 추첨이 열리고 5개의 표준 번호와 파워볼 번호로 구성된 우승 티켓이 당첨된다.
- 시상은 티켓의 당첨 번호 숫자 일치율을 기준으로 지급된다.
- 여기에서 구현할 컨트랙트는 상(Prize)에 대해서 전체 잔고 인출권을 부여할 것이다.
SimpleLottery
pragma solidity ^0.8.0;
contract SimpleLottery {
uint256 public constant TICKET_PRICE = 1e16; // 0.01 ether
address[] public tickets; // 티켓을 구매한 사용자 목록
address public winner; // 당첨자, 당첨자는 상금을 청구할 수 있다. 당첨자가 결정되기 전까지 상금 출금 불가하다.
// 복권 발권 과정에서 난수의 블록 해시를 알 수 없도록
// 이 시간(`ticketingClose`)으로부터 적어도 5분 후에 당첨자 추첨이 이뤄져야 한다.
uint256 public ticketingCloses;
constructor(uint256 duration) {
// `duration`은 초 단위로 입력 받아야 한다.
// `block.timestamp`가 UNIX 타임스탬프이기 때문이다.
ticketingCloses = block.timestamp + duration;
}
function buy() public payable {
require(msg.value == TICKET_PRICE);
require(block.timestamp < ticketingCloses);
tickets.push(msg.sender);
}
function drawWinner() public {
// 당첨자를 tickets에서 임의 선택한다.
// 당첨자를 뽑기 위해서는 티켓 발권이 종료되고 적어도 5분이 경과되야 한다.
// 이것은 티켓을 구입하는 동안 아무도 난수의 엔트로피 소스인 블록 해시를
// 알 수 없도록 하기 위함이다.
require(block.timestamp > ticketingCloses + 5 minutes);
// 아직 당첨자 추첨이 이루어지지 않은 것을 검증하기 위해
// 당첨자(`winner`)의 주소는 zero address로 설정돼 있음을 확인한다.
require(winner == address(0));
// 최근 블록 해시를 해시해 임의의 바이트열 `rand`를 생성한다.
bytes32 source = blockhash(block.number - 1);
bytes32 rand = keccak256(abi.encode(source));
// 생성한 바이트열을 정수로 변환해 나눗셈 연산을 사용해 계수 범위를 제한하면 무작위 인덱스가 생성된다.
// `tickets` 배열상에서 무작위 인덱스에 있는 주소가 당첨자 주소가 된다.
winner = tickets[uint256(rand) % tickets.length];
}
function withdraw() public {
require(msg.sender == winner);
payable(msg.sender).transfer(address(this).balance);
}
fallback() external payable {
buy();
}
}
RecurringLottery
pragma solidity ^0.8.0;
contract RecurringLottery {
struct Round {
// endBlock에 해당하는 번호의 블록이 채굴되면 한 라운드 종료
// 이 과정에서 무작위로 당첨자가 결정된다.
uint256 endBlock; // 라운드 종료 시간 역할
// 당첨자를 결정하는 난수 시드는 drawBlock에 해당하는 번호의 블록에서 나오는 블록 해시
uint256 drawBlock; // 추첨 시간 역할
Entry[] entries;
// 하나의 Entry가 둘 이상의 ticket을 보유할 수 있기에
// Entry를 바탕으로 판매된 총 티켓 수를 계산하려면 Entry의 수가 늘어남에 따라
// 계산 비용이 많이 들게 된다.
// 그래서 대신 Round 구조체 내의 `totalQuantity`를 정의해
// 각 라운드에서 판매되는 티켓 수를 추적하게 했다.
uint256 totalQuantity;
address winner;
}
struct Entry {
address buyer; // 구매자 주소
uint256 quantity; // 구입한 티켓 수량
}
uint256 public constant TICKET_PRICE = 1e15;
uint256 public round;
uint256 public duration; // 블록 내 단일 라운드 지속 시간. 24시간은 약 5500블록
mapping(address => uint256) public balances; // 사용자 잔고를 담는 표준 매핑
mapping(uint256 => Round) public rounds; // round를 Rounds 구조체로 연결하는 매핑 변수
// duration is in blocks. 1 day = ~5500 blocks
// `duration`은 블록 단위의 시간으로, 1일은 약 5500 블록에 해당한다.
constructor(uint256 _duration) {
// duration을 비롯한 시간은 초 단위가 아니라 블록 단위로 측정한다.
// 이는 티켓 구매 시점과 추첨 시점 사이의 초 경과가 아닌 블록 수를 고려하기 때문이다.
duration = _duration;
round = 1;
// endBlock과 drawBlock 사이의 시간 간격은 5 블록으로 설정돼,
// 어떤 참가자도 블록 해시를 알 수 없다.
// endBlock에 해당하는 번호의 블록이 채굴되면 한 라운드 종료
// 이 과정에서 무작위로 당첨자가 결정된다.
rounds[round].endBlock = block.number + duration;
// 당첨자를 결정하는 난수 시드는 drawBlock에 해당하는 번호의 블록에서 나오는 블록 해시
rounds[round].drawBlock = block.number + duration + 5;
}
function buy() public payable {
// buy()는 라운드 증가 로직이다.
// 먼저, 트랜잭션의 이더 값이 티켓 가격의 배수인지 확인한다.
// 한 번에 여러 티켓을 구입할 수 있지만 하나의 티켓을 쪼개서 구입할 수는 없다.
require(
msg.value % TICKET_PRICE == 0,
"It must be a multiple of the ticket price."
);
// 현재 라운드의 endBlock보다 현재 블록 번호가 크면 한 라운드가 만료된 것
// (현재 라운드가 만료됐는지 여부를 확인)
if (block.number > rounds[round].endBlock) {
// 현재 라운드가 만료됐을 경우 라운드 카운터를 증가
round += 1;
// 새로운 라운드의 종료 시간과 추첨 시간을 설정한다.
rounds[round].endBlock = block.number + duration;
rounds[round].drawBlock = block.number + duration + 5;
}
// 새로운 라운드라면 이 구매는 새로운 라운드의 첫 구매가 될 것이다.
uint256 quantity = msg.value / TICKET_PRICE;
Entry memory entry = Entry(msg.sender, quantity); // 한 구매자의 총 티켓 구매량을 entry 메모리에 담아두고,
rounds[round].entries.push(entry); // 해당 라운드에 매핑된 Round 구조체의 Entry[]에 entry 메모리 값을 넣음
rounds[round].totalQuantity += quantity; // 해당 라운드에 매핑된 총 구매량에 한 구매자의 총 티켓 구매량을 더해줌
}
function drawWinner(uint256 roundNumber) public {
// 당첨자 추첨을 위한 `drawWinner()` 함수의 초기 조건 및 로컬 변수 선언
Round storage drawing = rounds[roundNumber];
// 현재 회차의 당첨자는 아직 없어야 하므로 zero addrees와 비교한다.
require(drawing.winner == address(0));
// 당첨자를 결정하는 난수 시드인 drawBlock이 있어야 당첨자를 추첨할 수 있으므로(블록 해시를 얻는다.)
// 현재 블록 번호는 drawBlock의 블록 번호보다 높아야 한다.
require(block.number > drawing.drawBlock);
// 현재 회차에 당첨자를 추첨하기 위해선 1명 이상의 티켓 구매자들이 존재해야 한다.
require(drawing.entries.length > 0);
// pick winner
// 승자 선정
bytes32 source = blockhash(drawing.drawBlock);
// 난수 생성기는 추첨이 언제 이루어지는지에 관계없이
// drawBlock의 블록 해시를 사용해 임의의 시드를 생성한다.
bytes32 rand = keccak256(abi.encode(source));
// `counter`는 당첨 티켓에 해당하며, 이 당첨 티켓이 속하는 주소를 결정해야 한다.
uint256 counter = uint256(rand) % drawing.totalQuantity;
for (uint256 i = 0; i < drawing.entries.length; i++) {
uint256 quantity = drawing.entries[i].quantity;
if (quantity > counter) {
drawing.winner = drawing.entries[i].buyer;
break;
} else counter -= quantity;
}
balances[drawing.winner] += TICKET_PRICE * drawing.totalQuantity;
}
function withdraw() public {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
function deleteRound(uint256 _round) public {
/**
라운드가 완료되고 당첨자가 상금을 수령하고 나면 해당 라운드는 더이상 필요치 않다.
컨트랙트가 유명해지면 상태 정보의 크기가 매우 커질 수 있으므로 deleteRound()를 통해
오래된 데이터를 정리함으로써 블록체인 시민 의식을 준수할 수 있다.
*/
require(block.number > rounds[_round].drawBlock + 100);
require(rounds[_round].winner != address(0));
delete rounds[_round];
}
fallback() external payable {
buy();
}
}
RNGLottery
pragma solidity ^0.8.0;
contract RNGLottery {
uint256 public constant TICKET_PRICE = 1e16;
address[] public tickets;
address public winner;
// `seed` 는 당첨자를 결정하는 데 사용할 임의의 시드다.
// 새로운 비밀번호가 제출될 때마다 시드는 이를 포함하도록 업데이트 된다.
bytes32 public seed;
// 모든 티켓 구매자는 구매에 대한 약정 해시를 제출한다.
// 제출된 약정 해시는 이 매핑 변수 `commitments` 에 저장된다.
mapping(address => bytes32) public commitments;
// RecurringLottery의 endBlock과 동일하다.
// 이 블록 번호가 경과하고 나면 티켓을 구매할 수 없다.
uint256 public ticketDeadline; // 티켓 판매 종료 데드라인
// 약정 해시 공개는 이 컨트랙트에서 도입된 새로운 요소이다.
// 약정 해시 공개에 대한 마감도 필요하다.
// 이 변수는 해시 공개 마감 시간에 해당하며 모든 공개는 티켓 마감일과
// 공개 마감 전에 이루어져야 한다.
uint256 public revealDeadline;
constructor(uint256 duration, uint256 revealDuration) {
ticketDeadline = block.number + duration;
revealDeadline = ticketDeadline + revealDuration;
}
/**
사용자는 티켓을 구매하기 전에 먼저 약정 해시를 제출한다.
이 함수는 주소와 비밀번호 N으로 약정 해시를 생성한다.
해당 주소는 사용자의 주소여야 하므로 공개 단계에서 약정 해시가 유효한지
체인상에서 제대로 확인할 수 있다.
사용자의 주소는 비밀번호와 함께 해시된다. 이는 비밀번호 저장에 salt가 사용됨과 같다.
숫자만 사용하면 사전 방식 공격으로 약정 정보가 노출될 수 있기 때문이다.
대신 사용자 주소를 추가하면 공격자의 레인보우 테이블 공격을 막을 수 있다.
함수에서 pure 키워드를 사용하는 것은 이번이 처음이다.
pure의 출력은 함수의 인수에만 의존한다.
그 덕분에 pure 함수를 호출할 때는 트랜잭션을 전송할 필요가 없다.
pure 함수는 상태 트리를 업데이트하거나 합의 프로토콜을 거치지 않는다.
로컬에서 결과를 계산하고 사용해야만 한다.
*/
function createCommitment(address user, uint256 N)
public
pure
returns (bytes32 commitment)
{
// bytes memory source = abi.encodePacked(user, N);
/**
abi.encodePacked(arg)가 a, b라는 arg를 받을 때, a와 b 둘 중 하나가 동적 유형인지 확인해야 한다.
a, b 중에 하나가 동적 유형이라면 입력 순서에 따라 결과값이 다를 수 있다.
따라서 abi.encode(arg)를 사용할 것을 권장한다.
*/
bytes memory source = abi.encode(user, N);
return keccak256(source); // 약정 해시를 32bytes의 16진수 문자열로 나타낸다. 이 약정 해시가 티켓 구매 트랜잭션에 사용된다.
}
function buy(bytes32 commitment) public payable {
require(msg.value == TICKET_PRICE);
require(block.number <= ticketDeadline);
commitments[msg.sender] = commitment;
}
function reveal(uint256 N) public {
// 비밀번호를 공개하고 검증한다.
// 블록 넘버는 티켓 구매 마감 후에
// 약정 해시 공개 기간은 지나지 않아야 한다.
require(block.number > ticketDeadline);
require(block.number <= revealDeadline);
// 입력한 비멀번호와 함수를 호출한 사용자 주소를 검증할 약정 해시를 생성하고
bytes32 hash = createCommitment(msg.sender, N);
// 생성된 약정 해시가 과거에 사용자가 만든 약정 해시와 같은지 검사한다.
require(hash == commitments[msg.sender]);
// 같다면 비밀번호를 현재 seed와 합치고
// 합쳐진 바이트 시퀀스인 source를 해시해 새로운 seed로 업데이트 한다.
bytes memory source = abi.encode(seed, N);
seed = keccak256(source);
tickets.push(msg.sender);
}
function drawWinner() public {
require(block.number > revealDeadline);
require(winner == address(0));
uint256 randIndex = uint256(seed) % tickets.length;
winner = tickets[randIndex];
}
function withdraw() public {
require(msg.sender == winner);
payable(msg.sender).transfer(address(this).balance);
}
}
PowerBall
pragma solidity ^0.8.0;
/**
파워볼 컨트랙트는 복권 컨트랙트 중에서 가장 복잡하다.
티켓 구매와 다중 당첨금 지급을 모두 가진 순환 복권이다.
*/
contract Powerball {
struct Round {
/**
Round 구조체는 RecurringLottery과 유사하다.
*/
uint256 endTime; // 티켓 구매 기한
uint256 drawBlock; // 임의 숫자를 생성하는 데 사용할 미래의 블록 번호
uint256[6] winningNumbers; // 여섯 개의 당첨번호 배열
mapping(address => uint256[6][]) tickets; // 하나의 플레이어가 여러 티켓을 가질 수 있는 티켓 매핑
/**
솔리디티에서 다차원 배열은 자바나 C와는 다르다.
솔리디티에서 uint256[6][]은 uint256[6]개의 원소를 갖는 동적 배열을 의미한다.
*/
}
uint256 public constant TICKET_PRICE = 2e15; // 단일 티켓 가격, 0.002이더
uint256 public constant MAX_NUMBER = 69;
uint256 public constant MAX_POWERBALL_NUMBER = 26;
uint256 public constant ROUND_LENGTH = 3 days; // 라운드 길이의 초 단위, 게임이 진행될 시간
uint256 public round; // 현재 라운드
mapping(uint256 => Round) public rounds; // 현재 라운드에 대한 정보를 Round 구조체로 매핑
constructor() {
round = 1;
rounds[round].endTime = block.timestamp + ROUND_LENGTH;
}
/**
buy() 함수의 전반부는 입력 데이터에 대한 일련의 검사를 진행한다.
*/
function buy(uint256[6][] memory numbers) public payable {
// 구매할 티켓 가격이 전달한 이더와 매치가 되는지 확인한다.
require(numbers.length * TICKET_PRICE == msg.value);
// Ensure the non-powerball numbers on each ticket are unique
// 티켓의 각 번호가 유니크한지 검사한다.
for (uint256 k = 0; k < numbers.length; k++) {
for (uint256 i = 0; i < 4; i++) {
for (uint256 j = i + 1; j < 5; j++) {
require(numbers[k][i] != numbers[k][j]);
}
}
}
// Ensure the picked numbers are within the acceptable range
// 티켓의 각 번호가 적절한 범위 내에 있는지 검사한다.
for (uint256 i = 0; i < numbers.length; i++) {
for (uint256 j = 0; j < 6; j++) require(numbers[i][j] > 0); // 각 번호는 0보다 커야함
for (uint256 j = 0; j < 5; j++)
require(numbers[i][j] <= MAX_NUMBER); // 티켓 번호는 MAX_NUMBER보단 작아야 함
require(numbers[i][5] <= MAX_POWERBALL_NUMBER); // 파워볼 번호(마지막 자리)는 최대 파워볼 번호보다 작아야 함
}
// check for round expiry
// 라운드 마감 확인
if (block.timestamp > rounds[round].endTime) {
/**
여기서 해결해야 할 문제는 drawBlock이다.
drawBlock은 추첨 기간과 같은 역할을 한다.
그러나 이 컨트랙트에서 한 라운드의 drawBlock은 다음 라운드의 첫번째 티켓 구매를 할 때까지 설정되지 않는다.
이는 아무도 다음 라운드 티켓을 구매하지 않으면
한 라운드를 위한 추첨이 영원히 지연됨을 의미한다.
또한 누군가 임의로 다음 라운드 티켓을 구매해 추첨을 트리거할 수도 있음을 의미한다.
*/
rounds[round].drawBlock = block.number + 5;
round += 1;
rounds[round].endTime = block.timestamp + ROUND_LENGTH;
}
// 검사에 합격하면 해당 티켓으로 컨트랙트 상태를 업데이트한다.
// 위에서 라운드 설정이 완료되면 해당 라운드의 티켓 풀에 티켓이 하나씩 들어간다.
for (uint256 i = 0; i < numbers.length; i++)
rounds[round].tickets[msg.sender].push(numbers[i]);
}
function drawNumbers(uint256 _round) public {
// 이 함수는 해당 라운드의 우승 티켓이 될 6개 번호를 무작위로 추첨한다.
uint256 drawBlock = rounds[_round].drawBlock; // 해당 라운드에 drawBlock이 있어야 추첨 진행을 했다를 의미함
require(block.timestamp > rounds[_round].endTime); // 티켓 구매 마감이 되어야 함
require(block.number >= drawBlock); // 추첨 기간이 끝나야 함
require(rounds[_round].winningNumbers[0] == 0); //당첨 번호는 아직 설정되지 않았어야 함. 따라서 0과 비교함
uint256 i = 0;
uint256 seed = 0;
// 파워볼 당첨 번호를 추첨한다.
while (i < 5) {
// 이 로직은 drawBlock으로부터 256블록(약 80분) 내에 실행되어야 한다.
bytes32 source = blockhash(drawBlock);
/**
난수를 생성할 때 매번 동일한 블록해시를 재사용할 수 없다.
그렇게 되면 동일한 수가 5번 연속으로 출력될 것이기 때문이다.
그 대신 블록 수에 매번 고유한 숫자(이 경우 i)를 연결해 만든 바이트 문자열을 해시해 시드를 얻는다.
*/
bytes memory encodedSource = abi.encode(source, seed);
bytes32 _rand = keccak256(encodedSource);
uint256 numberDraw = (uint256(_rand) % MAX_NUMBER) + 1;
// non-powerball numbers must be unique
bool duplicate = false;
for (uint256 j = 0; j < i; j++) {
if (numberDraw == rounds[_round].winningNumbers[j]) {
duplicate = true;
seed++;
break;
}
}
if (duplicate) continue;
rounds[_round].winningNumbers[i] = numberDraw;
i++;
seed++;
}
bytes32 source = blockhash(drawBlock);
bytes memory encodedSource = abi.encode(source, seed);
bytes32 _rand = keccak256(encodedSource);
uint256 powerballDraw = (uint256(_rand) % MAX_POWERBALL_NUMBER) + 1;
rounds[_round].winningNumbers[5] = powerballDraw;
}
/**
번호 추첨이 끝난 후, 당첨 티켓을 소지한 사용자는 해당 라운드에 대한
보상을 청구할 수 있다.
*/
function claim(uint256 _round) public {
require(rounds[_round].tickets[msg.sender].length > 0); // 당첨자는 티켓을 소유하고 있어야 하고,
require(rounds[_round].winningNumbers[0] != 0); // 당첨번호가 0이 아니어야 한다. (이는 당첨 번호가 추첨된 상태임을 의미)
uint256[6][] storage myNumbers = rounds[_round].tickets[msg.sender];
uint256[6] storage winningNumbers = rounds[_round].winningNumbers;
uint256 payout = 0;
for (uint256 i = 0; i < myNumbers.length; i++) {
uint256 numberMatches = 0;
for (uint256 j = 0; j < 5; j++) {
for (uint256 k = 0; k < 5; k++) {
if (myNumbers[i][j] == winningNumbers[k])
numberMatches += 1;
}
}
bool powerballMatches = (myNumbers[i][5] == winningNumbers[5]);
// win conditions
if (numberMatches == 5 && powerballMatches) {
payout = address(this).balance;
break;
} else if (numberMatches == 5) payout += 1000 ether;
else if (numberMatches == 4 && powerballMatches) payout += 50 ether;
else if (numberMatches == 4)
payout += 1e17; // .1 ether
else if (numberMatches == 3 && powerballMatches)
payout += 1e17; // .1 ether
else if (numberMatches == 3)
payout += 7e15; // .007 ether
else if (numberMatches == 2 && powerballMatches)
payout += 7e15; // .007 ether
else if (powerballMatches) payout += 4e15; // .004 ether
}
// 설정된 payout으로 최종 상금 지급후 해당 라운드의 사용자 티켓을 삭제해
// 중복 수령을 방지한다.
delete rounds[_round].tickets[msg.sender];
payable(msg.sender).transfer(payout);
}
/**
솔리디티의 구조체가 public이라면 getter를 자동으로 생성해준다.
그러나 솔리디티는 반환되는 배열에 복합 자료형을 포함할 수 없다.
따라서 매핑과 배열의 경우 자체 view() 함수를 생성해줘야 한다.
*/
function ticketsFor(uint256 _round, address user)
public
view
returns (uint256[6][] memory tickets)
{
return rounds[_round].tickets[user];
}
function winningNumbersFor(uint256 _round)
public
view
returns (uint256[6] memory winningNumbers)
{
return rounds[_round].winningNumbers;
}
}
Author And Source
이 문제에 관하여(Solidity v0.8.0 기반의 복권 컨트랙트 개발), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@kimh4nkyul/Solidity-v0.8.0-기반의-복권-컨트랙트-개발
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
pragma solidity ^0.8.0;
contract SimpleLottery {
uint256 public constant TICKET_PRICE = 1e16; // 0.01 ether
address[] public tickets; // 티켓을 구매한 사용자 목록
address public winner; // 당첨자, 당첨자는 상금을 청구할 수 있다. 당첨자가 결정되기 전까지 상금 출금 불가하다.
// 복권 발권 과정에서 난수의 블록 해시를 알 수 없도록
// 이 시간(`ticketingClose`)으로부터 적어도 5분 후에 당첨자 추첨이 이뤄져야 한다.
uint256 public ticketingCloses;
constructor(uint256 duration) {
// `duration`은 초 단위로 입력 받아야 한다.
// `block.timestamp`가 UNIX 타임스탬프이기 때문이다.
ticketingCloses = block.timestamp + duration;
}
function buy() public payable {
require(msg.value == TICKET_PRICE);
require(block.timestamp < ticketingCloses);
tickets.push(msg.sender);
}
function drawWinner() public {
// 당첨자를 tickets에서 임의 선택한다.
// 당첨자를 뽑기 위해서는 티켓 발권이 종료되고 적어도 5분이 경과되야 한다.
// 이것은 티켓을 구입하는 동안 아무도 난수의 엔트로피 소스인 블록 해시를
// 알 수 없도록 하기 위함이다.
require(block.timestamp > ticketingCloses + 5 minutes);
// 아직 당첨자 추첨이 이루어지지 않은 것을 검증하기 위해
// 당첨자(`winner`)의 주소는 zero address로 설정돼 있음을 확인한다.
require(winner == address(0));
// 최근 블록 해시를 해시해 임의의 바이트열 `rand`를 생성한다.
bytes32 source = blockhash(block.number - 1);
bytes32 rand = keccak256(abi.encode(source));
// 생성한 바이트열을 정수로 변환해 나눗셈 연산을 사용해 계수 범위를 제한하면 무작위 인덱스가 생성된다.
// `tickets` 배열상에서 무작위 인덱스에 있는 주소가 당첨자 주소가 된다.
winner = tickets[uint256(rand) % tickets.length];
}
function withdraw() public {
require(msg.sender == winner);
payable(msg.sender).transfer(address(this).balance);
}
fallback() external payable {
buy();
}
}
pragma solidity ^0.8.0;
contract RecurringLottery {
struct Round {
// endBlock에 해당하는 번호의 블록이 채굴되면 한 라운드 종료
// 이 과정에서 무작위로 당첨자가 결정된다.
uint256 endBlock; // 라운드 종료 시간 역할
// 당첨자를 결정하는 난수 시드는 drawBlock에 해당하는 번호의 블록에서 나오는 블록 해시
uint256 drawBlock; // 추첨 시간 역할
Entry[] entries;
// 하나의 Entry가 둘 이상의 ticket을 보유할 수 있기에
// Entry를 바탕으로 판매된 총 티켓 수를 계산하려면 Entry의 수가 늘어남에 따라
// 계산 비용이 많이 들게 된다.
// 그래서 대신 Round 구조체 내의 `totalQuantity`를 정의해
// 각 라운드에서 판매되는 티켓 수를 추적하게 했다.
uint256 totalQuantity;
address winner;
}
struct Entry {
address buyer; // 구매자 주소
uint256 quantity; // 구입한 티켓 수량
}
uint256 public constant TICKET_PRICE = 1e15;
uint256 public round;
uint256 public duration; // 블록 내 단일 라운드 지속 시간. 24시간은 약 5500블록
mapping(address => uint256) public balances; // 사용자 잔고를 담는 표준 매핑
mapping(uint256 => Round) public rounds; // round를 Rounds 구조체로 연결하는 매핑 변수
// duration is in blocks. 1 day = ~5500 blocks
// `duration`은 블록 단위의 시간으로, 1일은 약 5500 블록에 해당한다.
constructor(uint256 _duration) {
// duration을 비롯한 시간은 초 단위가 아니라 블록 단위로 측정한다.
// 이는 티켓 구매 시점과 추첨 시점 사이의 초 경과가 아닌 블록 수를 고려하기 때문이다.
duration = _duration;
round = 1;
// endBlock과 drawBlock 사이의 시간 간격은 5 블록으로 설정돼,
// 어떤 참가자도 블록 해시를 알 수 없다.
// endBlock에 해당하는 번호의 블록이 채굴되면 한 라운드 종료
// 이 과정에서 무작위로 당첨자가 결정된다.
rounds[round].endBlock = block.number + duration;
// 당첨자를 결정하는 난수 시드는 drawBlock에 해당하는 번호의 블록에서 나오는 블록 해시
rounds[round].drawBlock = block.number + duration + 5;
}
function buy() public payable {
// buy()는 라운드 증가 로직이다.
// 먼저, 트랜잭션의 이더 값이 티켓 가격의 배수인지 확인한다.
// 한 번에 여러 티켓을 구입할 수 있지만 하나의 티켓을 쪼개서 구입할 수는 없다.
require(
msg.value % TICKET_PRICE == 0,
"It must be a multiple of the ticket price."
);
// 현재 라운드의 endBlock보다 현재 블록 번호가 크면 한 라운드가 만료된 것
// (현재 라운드가 만료됐는지 여부를 확인)
if (block.number > rounds[round].endBlock) {
// 현재 라운드가 만료됐을 경우 라운드 카운터를 증가
round += 1;
// 새로운 라운드의 종료 시간과 추첨 시간을 설정한다.
rounds[round].endBlock = block.number + duration;
rounds[round].drawBlock = block.number + duration + 5;
}
// 새로운 라운드라면 이 구매는 새로운 라운드의 첫 구매가 될 것이다.
uint256 quantity = msg.value / TICKET_PRICE;
Entry memory entry = Entry(msg.sender, quantity); // 한 구매자의 총 티켓 구매량을 entry 메모리에 담아두고,
rounds[round].entries.push(entry); // 해당 라운드에 매핑된 Round 구조체의 Entry[]에 entry 메모리 값을 넣음
rounds[round].totalQuantity += quantity; // 해당 라운드에 매핑된 총 구매량에 한 구매자의 총 티켓 구매량을 더해줌
}
function drawWinner(uint256 roundNumber) public {
// 당첨자 추첨을 위한 `drawWinner()` 함수의 초기 조건 및 로컬 변수 선언
Round storage drawing = rounds[roundNumber];
// 현재 회차의 당첨자는 아직 없어야 하므로 zero addrees와 비교한다.
require(drawing.winner == address(0));
// 당첨자를 결정하는 난수 시드인 drawBlock이 있어야 당첨자를 추첨할 수 있으므로(블록 해시를 얻는다.)
// 현재 블록 번호는 drawBlock의 블록 번호보다 높아야 한다.
require(block.number > drawing.drawBlock);
// 현재 회차에 당첨자를 추첨하기 위해선 1명 이상의 티켓 구매자들이 존재해야 한다.
require(drawing.entries.length > 0);
// pick winner
// 승자 선정
bytes32 source = blockhash(drawing.drawBlock);
// 난수 생성기는 추첨이 언제 이루어지는지에 관계없이
// drawBlock의 블록 해시를 사용해 임의의 시드를 생성한다.
bytes32 rand = keccak256(abi.encode(source));
// `counter`는 당첨 티켓에 해당하며, 이 당첨 티켓이 속하는 주소를 결정해야 한다.
uint256 counter = uint256(rand) % drawing.totalQuantity;
for (uint256 i = 0; i < drawing.entries.length; i++) {
uint256 quantity = drawing.entries[i].quantity;
if (quantity > counter) {
drawing.winner = drawing.entries[i].buyer;
break;
} else counter -= quantity;
}
balances[drawing.winner] += TICKET_PRICE * drawing.totalQuantity;
}
function withdraw() public {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
function deleteRound(uint256 _round) public {
/**
라운드가 완료되고 당첨자가 상금을 수령하고 나면 해당 라운드는 더이상 필요치 않다.
컨트랙트가 유명해지면 상태 정보의 크기가 매우 커질 수 있으므로 deleteRound()를 통해
오래된 데이터를 정리함으로써 블록체인 시민 의식을 준수할 수 있다.
*/
require(block.number > rounds[_round].drawBlock + 100);
require(rounds[_round].winner != address(0));
delete rounds[_round];
}
fallback() external payable {
buy();
}
}
RNGLottery
pragma solidity ^0.8.0;
contract RNGLottery {
uint256 public constant TICKET_PRICE = 1e16;
address[] public tickets;
address public winner;
// `seed` 는 당첨자를 결정하는 데 사용할 임의의 시드다.
// 새로운 비밀번호가 제출될 때마다 시드는 이를 포함하도록 업데이트 된다.
bytes32 public seed;
// 모든 티켓 구매자는 구매에 대한 약정 해시를 제출한다.
// 제출된 약정 해시는 이 매핑 변수 `commitments` 에 저장된다.
mapping(address => bytes32) public commitments;
// RecurringLottery의 endBlock과 동일하다.
// 이 블록 번호가 경과하고 나면 티켓을 구매할 수 없다.
uint256 public ticketDeadline; // 티켓 판매 종료 데드라인
// 약정 해시 공개는 이 컨트랙트에서 도입된 새로운 요소이다.
// 약정 해시 공개에 대한 마감도 필요하다.
// 이 변수는 해시 공개 마감 시간에 해당하며 모든 공개는 티켓 마감일과
// 공개 마감 전에 이루어져야 한다.
uint256 public revealDeadline;
constructor(uint256 duration, uint256 revealDuration) {
ticketDeadline = block.number + duration;
revealDeadline = ticketDeadline + revealDuration;
}
/**
사용자는 티켓을 구매하기 전에 먼저 약정 해시를 제출한다.
이 함수는 주소와 비밀번호 N으로 약정 해시를 생성한다.
해당 주소는 사용자의 주소여야 하므로 공개 단계에서 약정 해시가 유효한지
체인상에서 제대로 확인할 수 있다.
사용자의 주소는 비밀번호와 함께 해시된다. 이는 비밀번호 저장에 salt가 사용됨과 같다.
숫자만 사용하면 사전 방식 공격으로 약정 정보가 노출될 수 있기 때문이다.
대신 사용자 주소를 추가하면 공격자의 레인보우 테이블 공격을 막을 수 있다.
함수에서 pure 키워드를 사용하는 것은 이번이 처음이다.
pure의 출력은 함수의 인수에만 의존한다.
그 덕분에 pure 함수를 호출할 때는 트랜잭션을 전송할 필요가 없다.
pure 함수는 상태 트리를 업데이트하거나 합의 프로토콜을 거치지 않는다.
로컬에서 결과를 계산하고 사용해야만 한다.
*/
function createCommitment(address user, uint256 N)
public
pure
returns (bytes32 commitment)
{
// bytes memory source = abi.encodePacked(user, N);
/**
abi.encodePacked(arg)가 a, b라는 arg를 받을 때, a와 b 둘 중 하나가 동적 유형인지 확인해야 한다.
a, b 중에 하나가 동적 유형이라면 입력 순서에 따라 결과값이 다를 수 있다.
따라서 abi.encode(arg)를 사용할 것을 권장한다.
*/
bytes memory source = abi.encode(user, N);
return keccak256(source); // 약정 해시를 32bytes의 16진수 문자열로 나타낸다. 이 약정 해시가 티켓 구매 트랜잭션에 사용된다.
}
function buy(bytes32 commitment) public payable {
require(msg.value == TICKET_PRICE);
require(block.number <= ticketDeadline);
commitments[msg.sender] = commitment;
}
function reveal(uint256 N) public {
// 비밀번호를 공개하고 검증한다.
// 블록 넘버는 티켓 구매 마감 후에
// 약정 해시 공개 기간은 지나지 않아야 한다.
require(block.number > ticketDeadline);
require(block.number <= revealDeadline);
// 입력한 비멀번호와 함수를 호출한 사용자 주소를 검증할 약정 해시를 생성하고
bytes32 hash = createCommitment(msg.sender, N);
// 생성된 약정 해시가 과거에 사용자가 만든 약정 해시와 같은지 검사한다.
require(hash == commitments[msg.sender]);
// 같다면 비밀번호를 현재 seed와 합치고
// 합쳐진 바이트 시퀀스인 source를 해시해 새로운 seed로 업데이트 한다.
bytes memory source = abi.encode(seed, N);
seed = keccak256(source);
tickets.push(msg.sender);
}
function drawWinner() public {
require(block.number > revealDeadline);
require(winner == address(0));
uint256 randIndex = uint256(seed) % tickets.length;
winner = tickets[randIndex];
}
function withdraw() public {
require(msg.sender == winner);
payable(msg.sender).transfer(address(this).balance);
}
}
PowerBall
pragma solidity ^0.8.0;
/**
파워볼 컨트랙트는 복권 컨트랙트 중에서 가장 복잡하다.
티켓 구매와 다중 당첨금 지급을 모두 가진 순환 복권이다.
*/
contract Powerball {
struct Round {
/**
Round 구조체는 RecurringLottery과 유사하다.
*/
uint256 endTime; // 티켓 구매 기한
uint256 drawBlock; // 임의 숫자를 생성하는 데 사용할 미래의 블록 번호
uint256[6] winningNumbers; // 여섯 개의 당첨번호 배열
mapping(address => uint256[6][]) tickets; // 하나의 플레이어가 여러 티켓을 가질 수 있는 티켓 매핑
/**
솔리디티에서 다차원 배열은 자바나 C와는 다르다.
솔리디티에서 uint256[6][]은 uint256[6]개의 원소를 갖는 동적 배열을 의미한다.
*/
}
uint256 public constant TICKET_PRICE = 2e15; // 단일 티켓 가격, 0.002이더
uint256 public constant MAX_NUMBER = 69;
uint256 public constant MAX_POWERBALL_NUMBER = 26;
uint256 public constant ROUND_LENGTH = 3 days; // 라운드 길이의 초 단위, 게임이 진행될 시간
uint256 public round; // 현재 라운드
mapping(uint256 => Round) public rounds; // 현재 라운드에 대한 정보를 Round 구조체로 매핑
constructor() {
round = 1;
rounds[round].endTime = block.timestamp + ROUND_LENGTH;
}
/**
buy() 함수의 전반부는 입력 데이터에 대한 일련의 검사를 진행한다.
*/
function buy(uint256[6][] memory numbers) public payable {
// 구매할 티켓 가격이 전달한 이더와 매치가 되는지 확인한다.
require(numbers.length * TICKET_PRICE == msg.value);
// Ensure the non-powerball numbers on each ticket are unique
// 티켓의 각 번호가 유니크한지 검사한다.
for (uint256 k = 0; k < numbers.length; k++) {
for (uint256 i = 0; i < 4; i++) {
for (uint256 j = i + 1; j < 5; j++) {
require(numbers[k][i] != numbers[k][j]);
}
}
}
// Ensure the picked numbers are within the acceptable range
// 티켓의 각 번호가 적절한 범위 내에 있는지 검사한다.
for (uint256 i = 0; i < numbers.length; i++) {
for (uint256 j = 0; j < 6; j++) require(numbers[i][j] > 0); // 각 번호는 0보다 커야함
for (uint256 j = 0; j < 5; j++)
require(numbers[i][j] <= MAX_NUMBER); // 티켓 번호는 MAX_NUMBER보단 작아야 함
require(numbers[i][5] <= MAX_POWERBALL_NUMBER); // 파워볼 번호(마지막 자리)는 최대 파워볼 번호보다 작아야 함
}
// check for round expiry
// 라운드 마감 확인
if (block.timestamp > rounds[round].endTime) {
/**
여기서 해결해야 할 문제는 drawBlock이다.
drawBlock은 추첨 기간과 같은 역할을 한다.
그러나 이 컨트랙트에서 한 라운드의 drawBlock은 다음 라운드의 첫번째 티켓 구매를 할 때까지 설정되지 않는다.
이는 아무도 다음 라운드 티켓을 구매하지 않으면
한 라운드를 위한 추첨이 영원히 지연됨을 의미한다.
또한 누군가 임의로 다음 라운드 티켓을 구매해 추첨을 트리거할 수도 있음을 의미한다.
*/
rounds[round].drawBlock = block.number + 5;
round += 1;
rounds[round].endTime = block.timestamp + ROUND_LENGTH;
}
// 검사에 합격하면 해당 티켓으로 컨트랙트 상태를 업데이트한다.
// 위에서 라운드 설정이 완료되면 해당 라운드의 티켓 풀에 티켓이 하나씩 들어간다.
for (uint256 i = 0; i < numbers.length; i++)
rounds[round].tickets[msg.sender].push(numbers[i]);
}
function drawNumbers(uint256 _round) public {
// 이 함수는 해당 라운드의 우승 티켓이 될 6개 번호를 무작위로 추첨한다.
uint256 drawBlock = rounds[_round].drawBlock; // 해당 라운드에 drawBlock이 있어야 추첨 진행을 했다를 의미함
require(block.timestamp > rounds[_round].endTime); // 티켓 구매 마감이 되어야 함
require(block.number >= drawBlock); // 추첨 기간이 끝나야 함
require(rounds[_round].winningNumbers[0] == 0); //당첨 번호는 아직 설정되지 않았어야 함. 따라서 0과 비교함
uint256 i = 0;
uint256 seed = 0;
// 파워볼 당첨 번호를 추첨한다.
while (i < 5) {
// 이 로직은 drawBlock으로부터 256블록(약 80분) 내에 실행되어야 한다.
bytes32 source = blockhash(drawBlock);
/**
난수를 생성할 때 매번 동일한 블록해시를 재사용할 수 없다.
그렇게 되면 동일한 수가 5번 연속으로 출력될 것이기 때문이다.
그 대신 블록 수에 매번 고유한 숫자(이 경우 i)를 연결해 만든 바이트 문자열을 해시해 시드를 얻는다.
*/
bytes memory encodedSource = abi.encode(source, seed);
bytes32 _rand = keccak256(encodedSource);
uint256 numberDraw = (uint256(_rand) % MAX_NUMBER) + 1;
// non-powerball numbers must be unique
bool duplicate = false;
for (uint256 j = 0; j < i; j++) {
if (numberDraw == rounds[_round].winningNumbers[j]) {
duplicate = true;
seed++;
break;
}
}
if (duplicate) continue;
rounds[_round].winningNumbers[i] = numberDraw;
i++;
seed++;
}
bytes32 source = blockhash(drawBlock);
bytes memory encodedSource = abi.encode(source, seed);
bytes32 _rand = keccak256(encodedSource);
uint256 powerballDraw = (uint256(_rand) % MAX_POWERBALL_NUMBER) + 1;
rounds[_round].winningNumbers[5] = powerballDraw;
}
/**
번호 추첨이 끝난 후, 당첨 티켓을 소지한 사용자는 해당 라운드에 대한
보상을 청구할 수 있다.
*/
function claim(uint256 _round) public {
require(rounds[_round].tickets[msg.sender].length > 0); // 당첨자는 티켓을 소유하고 있어야 하고,
require(rounds[_round].winningNumbers[0] != 0); // 당첨번호가 0이 아니어야 한다. (이는 당첨 번호가 추첨된 상태임을 의미)
uint256[6][] storage myNumbers = rounds[_round].tickets[msg.sender];
uint256[6] storage winningNumbers = rounds[_round].winningNumbers;
uint256 payout = 0;
for (uint256 i = 0; i < myNumbers.length; i++) {
uint256 numberMatches = 0;
for (uint256 j = 0; j < 5; j++) {
for (uint256 k = 0; k < 5; k++) {
if (myNumbers[i][j] == winningNumbers[k])
numberMatches += 1;
}
}
bool powerballMatches = (myNumbers[i][5] == winningNumbers[5]);
// win conditions
if (numberMatches == 5 && powerballMatches) {
payout = address(this).balance;
break;
} else if (numberMatches == 5) payout += 1000 ether;
else if (numberMatches == 4 && powerballMatches) payout += 50 ether;
else if (numberMatches == 4)
payout += 1e17; // .1 ether
else if (numberMatches == 3 && powerballMatches)
payout += 1e17; // .1 ether
else if (numberMatches == 3)
payout += 7e15; // .007 ether
else if (numberMatches == 2 && powerballMatches)
payout += 7e15; // .007 ether
else if (powerballMatches) payout += 4e15; // .004 ether
}
// 설정된 payout으로 최종 상금 지급후 해당 라운드의 사용자 티켓을 삭제해
// 중복 수령을 방지한다.
delete rounds[_round].tickets[msg.sender];
payable(msg.sender).transfer(payout);
}
/**
솔리디티의 구조체가 public이라면 getter를 자동으로 생성해준다.
그러나 솔리디티는 반환되는 배열에 복합 자료형을 포함할 수 없다.
따라서 매핑과 배열의 경우 자체 view() 함수를 생성해줘야 한다.
*/
function ticketsFor(uint256 _round, address user)
public
view
returns (uint256[6][] memory tickets)
{
return rounds[_round].tickets[user];
}
function winningNumbersFor(uint256 _round)
public
view
returns (uint256[6] memory winningNumbers)
{
return rounds[_round].winningNumbers;
}
}
Author And Source
이 문제에 관하여(Solidity v0.8.0 기반의 복권 컨트랙트 개발), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@kimh4nkyul/Solidity-v0.8.0-기반의-복권-컨트랙트-개발
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
pragma solidity ^0.8.0;
contract RNGLottery {
uint256 public constant TICKET_PRICE = 1e16;
address[] public tickets;
address public winner;
// `seed` 는 당첨자를 결정하는 데 사용할 임의의 시드다.
// 새로운 비밀번호가 제출될 때마다 시드는 이를 포함하도록 업데이트 된다.
bytes32 public seed;
// 모든 티켓 구매자는 구매에 대한 약정 해시를 제출한다.
// 제출된 약정 해시는 이 매핑 변수 `commitments` 에 저장된다.
mapping(address => bytes32) public commitments;
// RecurringLottery의 endBlock과 동일하다.
// 이 블록 번호가 경과하고 나면 티켓을 구매할 수 없다.
uint256 public ticketDeadline; // 티켓 판매 종료 데드라인
// 약정 해시 공개는 이 컨트랙트에서 도입된 새로운 요소이다.
// 약정 해시 공개에 대한 마감도 필요하다.
// 이 변수는 해시 공개 마감 시간에 해당하며 모든 공개는 티켓 마감일과
// 공개 마감 전에 이루어져야 한다.
uint256 public revealDeadline;
constructor(uint256 duration, uint256 revealDuration) {
ticketDeadline = block.number + duration;
revealDeadline = ticketDeadline + revealDuration;
}
/**
사용자는 티켓을 구매하기 전에 먼저 약정 해시를 제출한다.
이 함수는 주소와 비밀번호 N으로 약정 해시를 생성한다.
해당 주소는 사용자의 주소여야 하므로 공개 단계에서 약정 해시가 유효한지
체인상에서 제대로 확인할 수 있다.
사용자의 주소는 비밀번호와 함께 해시된다. 이는 비밀번호 저장에 salt가 사용됨과 같다.
숫자만 사용하면 사전 방식 공격으로 약정 정보가 노출될 수 있기 때문이다.
대신 사용자 주소를 추가하면 공격자의 레인보우 테이블 공격을 막을 수 있다.
함수에서 pure 키워드를 사용하는 것은 이번이 처음이다.
pure의 출력은 함수의 인수에만 의존한다.
그 덕분에 pure 함수를 호출할 때는 트랜잭션을 전송할 필요가 없다.
pure 함수는 상태 트리를 업데이트하거나 합의 프로토콜을 거치지 않는다.
로컬에서 결과를 계산하고 사용해야만 한다.
*/
function createCommitment(address user, uint256 N)
public
pure
returns (bytes32 commitment)
{
// bytes memory source = abi.encodePacked(user, N);
/**
abi.encodePacked(arg)가 a, b라는 arg를 받을 때, a와 b 둘 중 하나가 동적 유형인지 확인해야 한다.
a, b 중에 하나가 동적 유형이라면 입력 순서에 따라 결과값이 다를 수 있다.
따라서 abi.encode(arg)를 사용할 것을 권장한다.
*/
bytes memory source = abi.encode(user, N);
return keccak256(source); // 약정 해시를 32bytes의 16진수 문자열로 나타낸다. 이 약정 해시가 티켓 구매 트랜잭션에 사용된다.
}
function buy(bytes32 commitment) public payable {
require(msg.value == TICKET_PRICE);
require(block.number <= ticketDeadline);
commitments[msg.sender] = commitment;
}
function reveal(uint256 N) public {
// 비밀번호를 공개하고 검증한다.
// 블록 넘버는 티켓 구매 마감 후에
// 약정 해시 공개 기간은 지나지 않아야 한다.
require(block.number > ticketDeadline);
require(block.number <= revealDeadline);
// 입력한 비멀번호와 함수를 호출한 사용자 주소를 검증할 약정 해시를 생성하고
bytes32 hash = createCommitment(msg.sender, N);
// 생성된 약정 해시가 과거에 사용자가 만든 약정 해시와 같은지 검사한다.
require(hash == commitments[msg.sender]);
// 같다면 비밀번호를 현재 seed와 합치고
// 합쳐진 바이트 시퀀스인 source를 해시해 새로운 seed로 업데이트 한다.
bytes memory source = abi.encode(seed, N);
seed = keccak256(source);
tickets.push(msg.sender);
}
function drawWinner() public {
require(block.number > revealDeadline);
require(winner == address(0));
uint256 randIndex = uint256(seed) % tickets.length;
winner = tickets[randIndex];
}
function withdraw() public {
require(msg.sender == winner);
payable(msg.sender).transfer(address(this).balance);
}
}
pragma solidity ^0.8.0;
/**
파워볼 컨트랙트는 복권 컨트랙트 중에서 가장 복잡하다.
티켓 구매와 다중 당첨금 지급을 모두 가진 순환 복권이다.
*/
contract Powerball {
struct Round {
/**
Round 구조체는 RecurringLottery과 유사하다.
*/
uint256 endTime; // 티켓 구매 기한
uint256 drawBlock; // 임의 숫자를 생성하는 데 사용할 미래의 블록 번호
uint256[6] winningNumbers; // 여섯 개의 당첨번호 배열
mapping(address => uint256[6][]) tickets; // 하나의 플레이어가 여러 티켓을 가질 수 있는 티켓 매핑
/**
솔리디티에서 다차원 배열은 자바나 C와는 다르다.
솔리디티에서 uint256[6][]은 uint256[6]개의 원소를 갖는 동적 배열을 의미한다.
*/
}
uint256 public constant TICKET_PRICE = 2e15; // 단일 티켓 가격, 0.002이더
uint256 public constant MAX_NUMBER = 69;
uint256 public constant MAX_POWERBALL_NUMBER = 26;
uint256 public constant ROUND_LENGTH = 3 days; // 라운드 길이의 초 단위, 게임이 진행될 시간
uint256 public round; // 현재 라운드
mapping(uint256 => Round) public rounds; // 현재 라운드에 대한 정보를 Round 구조체로 매핑
constructor() {
round = 1;
rounds[round].endTime = block.timestamp + ROUND_LENGTH;
}
/**
buy() 함수의 전반부는 입력 데이터에 대한 일련의 검사를 진행한다.
*/
function buy(uint256[6][] memory numbers) public payable {
// 구매할 티켓 가격이 전달한 이더와 매치가 되는지 확인한다.
require(numbers.length * TICKET_PRICE == msg.value);
// Ensure the non-powerball numbers on each ticket are unique
// 티켓의 각 번호가 유니크한지 검사한다.
for (uint256 k = 0; k < numbers.length; k++) {
for (uint256 i = 0; i < 4; i++) {
for (uint256 j = i + 1; j < 5; j++) {
require(numbers[k][i] != numbers[k][j]);
}
}
}
// Ensure the picked numbers are within the acceptable range
// 티켓의 각 번호가 적절한 범위 내에 있는지 검사한다.
for (uint256 i = 0; i < numbers.length; i++) {
for (uint256 j = 0; j < 6; j++) require(numbers[i][j] > 0); // 각 번호는 0보다 커야함
for (uint256 j = 0; j < 5; j++)
require(numbers[i][j] <= MAX_NUMBER); // 티켓 번호는 MAX_NUMBER보단 작아야 함
require(numbers[i][5] <= MAX_POWERBALL_NUMBER); // 파워볼 번호(마지막 자리)는 최대 파워볼 번호보다 작아야 함
}
// check for round expiry
// 라운드 마감 확인
if (block.timestamp > rounds[round].endTime) {
/**
여기서 해결해야 할 문제는 drawBlock이다.
drawBlock은 추첨 기간과 같은 역할을 한다.
그러나 이 컨트랙트에서 한 라운드의 drawBlock은 다음 라운드의 첫번째 티켓 구매를 할 때까지 설정되지 않는다.
이는 아무도 다음 라운드 티켓을 구매하지 않으면
한 라운드를 위한 추첨이 영원히 지연됨을 의미한다.
또한 누군가 임의로 다음 라운드 티켓을 구매해 추첨을 트리거할 수도 있음을 의미한다.
*/
rounds[round].drawBlock = block.number + 5;
round += 1;
rounds[round].endTime = block.timestamp + ROUND_LENGTH;
}
// 검사에 합격하면 해당 티켓으로 컨트랙트 상태를 업데이트한다.
// 위에서 라운드 설정이 완료되면 해당 라운드의 티켓 풀에 티켓이 하나씩 들어간다.
for (uint256 i = 0; i < numbers.length; i++)
rounds[round].tickets[msg.sender].push(numbers[i]);
}
function drawNumbers(uint256 _round) public {
// 이 함수는 해당 라운드의 우승 티켓이 될 6개 번호를 무작위로 추첨한다.
uint256 drawBlock = rounds[_round].drawBlock; // 해당 라운드에 drawBlock이 있어야 추첨 진행을 했다를 의미함
require(block.timestamp > rounds[_round].endTime); // 티켓 구매 마감이 되어야 함
require(block.number >= drawBlock); // 추첨 기간이 끝나야 함
require(rounds[_round].winningNumbers[0] == 0); //당첨 번호는 아직 설정되지 않았어야 함. 따라서 0과 비교함
uint256 i = 0;
uint256 seed = 0;
// 파워볼 당첨 번호를 추첨한다.
while (i < 5) {
// 이 로직은 drawBlock으로부터 256블록(약 80분) 내에 실행되어야 한다.
bytes32 source = blockhash(drawBlock);
/**
난수를 생성할 때 매번 동일한 블록해시를 재사용할 수 없다.
그렇게 되면 동일한 수가 5번 연속으로 출력될 것이기 때문이다.
그 대신 블록 수에 매번 고유한 숫자(이 경우 i)를 연결해 만든 바이트 문자열을 해시해 시드를 얻는다.
*/
bytes memory encodedSource = abi.encode(source, seed);
bytes32 _rand = keccak256(encodedSource);
uint256 numberDraw = (uint256(_rand) % MAX_NUMBER) + 1;
// non-powerball numbers must be unique
bool duplicate = false;
for (uint256 j = 0; j < i; j++) {
if (numberDraw == rounds[_round].winningNumbers[j]) {
duplicate = true;
seed++;
break;
}
}
if (duplicate) continue;
rounds[_round].winningNumbers[i] = numberDraw;
i++;
seed++;
}
bytes32 source = blockhash(drawBlock);
bytes memory encodedSource = abi.encode(source, seed);
bytes32 _rand = keccak256(encodedSource);
uint256 powerballDraw = (uint256(_rand) % MAX_POWERBALL_NUMBER) + 1;
rounds[_round].winningNumbers[5] = powerballDraw;
}
/**
번호 추첨이 끝난 후, 당첨 티켓을 소지한 사용자는 해당 라운드에 대한
보상을 청구할 수 있다.
*/
function claim(uint256 _round) public {
require(rounds[_round].tickets[msg.sender].length > 0); // 당첨자는 티켓을 소유하고 있어야 하고,
require(rounds[_round].winningNumbers[0] != 0); // 당첨번호가 0이 아니어야 한다. (이는 당첨 번호가 추첨된 상태임을 의미)
uint256[6][] storage myNumbers = rounds[_round].tickets[msg.sender];
uint256[6] storage winningNumbers = rounds[_round].winningNumbers;
uint256 payout = 0;
for (uint256 i = 0; i < myNumbers.length; i++) {
uint256 numberMatches = 0;
for (uint256 j = 0; j < 5; j++) {
for (uint256 k = 0; k < 5; k++) {
if (myNumbers[i][j] == winningNumbers[k])
numberMatches += 1;
}
}
bool powerballMatches = (myNumbers[i][5] == winningNumbers[5]);
// win conditions
if (numberMatches == 5 && powerballMatches) {
payout = address(this).balance;
break;
} else if (numberMatches == 5) payout += 1000 ether;
else if (numberMatches == 4 && powerballMatches) payout += 50 ether;
else if (numberMatches == 4)
payout += 1e17; // .1 ether
else if (numberMatches == 3 && powerballMatches)
payout += 1e17; // .1 ether
else if (numberMatches == 3)
payout += 7e15; // .007 ether
else if (numberMatches == 2 && powerballMatches)
payout += 7e15; // .007 ether
else if (powerballMatches) payout += 4e15; // .004 ether
}
// 설정된 payout으로 최종 상금 지급후 해당 라운드의 사용자 티켓을 삭제해
// 중복 수령을 방지한다.
delete rounds[_round].tickets[msg.sender];
payable(msg.sender).transfer(payout);
}
/**
솔리디티의 구조체가 public이라면 getter를 자동으로 생성해준다.
그러나 솔리디티는 반환되는 배열에 복합 자료형을 포함할 수 없다.
따라서 매핑과 배열의 경우 자체 view() 함수를 생성해줘야 한다.
*/
function ticketsFor(uint256 _round, address user)
public
view
returns (uint256[6][] memory tickets)
{
return rounds[_round].tickets[user];
}
function winningNumbersFor(uint256 _round)
public
view
returns (uint256[6] memory winningNumbers)
{
return rounds[_round].winningNumbers;
}
}
Author And Source
이 문제에 관하여(Solidity v0.8.0 기반의 복권 컨트랙트 개발), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@kimh4nkyul/Solidity-v0.8.0-기반의-복권-컨트랙트-개발저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)