Разработка лотереи на блокчейне
Blockchain лотерея — один из наиболее естественных use cases для smart contracts: прозрачное хранение призового фонда, верифицируемый розыгрыш через Chainlink VRF, автоматические выплаты без посредников. В отличие от традиционных лотерей — никто не может манипулировать результатом, фонд нельзя украсть, выплата гарантирована кодом.
Типы лотерей
Fixed prize (Powerball-style): фиксированный джекпот, игрок выбирает числа, выигрывает при совпадении. Фонд берётся из продажи билетов + резерва оператора.
Prize pool lottery (Raffle): весь призовой фонд = сумма всех ставок (минус комиссия). Больше участников = больше приз. Один winner takes all или несколько призовых мест.
No-loss lottery (PoolTogether-style): участники вносят в yield-генерирующий протокол (AAVE), накопленный процент идёт winner, депозиты возвращаются всем. «Играешь бесплатно, рискуешь только доходностью».
Smart contract — раффл лотерея
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract BlockchainLottery is VRFConsumerBaseV2Plus {
struct Lottery {
uint256 ticketPrice;
uint256 startTime;
uint256 endTime;
uint256 maxTickets;
uint256 ticketsSold;
address[] participants; // array для weighted selection
uint256 prizePool;
LotteryStatus status;
uint256 vrfRequestId;
address winner;
uint256[] prizeDistribution; // [7000, 2000, 1000] = 70%, 20%, 10%
}
enum LotteryStatus { OPEN, DRAWING, CLOSED, CANCELLED }
mapping(uint256 => Lottery) public lotteries;
uint256 public lotteryCount;
// Комиссия оператора
uint256 public operatorFee = 300; // 3% в basis points
function createLottery(
uint256 ticketPrice,
uint256 duration,
uint256 maxTickets,
uint256[] calldata prizeDistribution
) external onlyOwner returns (uint256 lotteryId) {
require(_validateDistribution(prizeDistribution), "Invalid distribution");
lotteryId = ++lotteryCount;
lotteries[lotteryId] = Lottery({
ticketPrice: ticketPrice,
startTime: block.timestamp,
endTime: block.timestamp + duration,
maxTickets: maxTickets,
ticketsSold: 0,
participants: new address[](0),
prizePool: 0,
status: LotteryStatus.OPEN,
vrfRequestId: 0,
winner: address(0),
prizeDistribution: prizeDistribution,
});
}
function buyTickets(uint256 lotteryId, uint256 amount) external payable {
Lottery storage lottery = lotteries[lotteryId];
require(lottery.status == LotteryStatus.OPEN, "Not open");
require(block.timestamp < lottery.endTime, "Lottery ended");
require(lottery.ticketsSold + amount <= lottery.maxTickets, "Not enough tickets");
require(msg.value == lottery.ticketPrice * amount, "Wrong payment");
// Добавляем участника amount раз (weighted: больше билетов = больше шансов)
for (uint256 i = 0; i < amount; i++) {
lottery.participants.push(msg.sender);
}
lottery.ticketsSold += amount;
uint256 fee = (msg.value * operatorFee) / 10000;
lottery.prizePool += msg.value - fee;
emit TicketsPurchased(lotteryId, msg.sender, amount);
}
function drawWinners(uint256 lotteryId) external {
Lottery storage lottery = lotteries[lotteryId];
require(
block.timestamp >= lottery.endTime || lottery.ticketsSold == lottery.maxTickets,
"Lottery not ended"
);
require(lottery.status == LotteryStatus.OPEN, "Wrong status");
require(lottery.ticketsSold > 0, "No participants");
lottery.status = LotteryStatus.DRAWING;
// Запрашиваем N случайных чисел (по количеству призовых мест)
uint256 numWinners = lottery.prizeDistribution.length;
uint256 requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: SUBSCRIPTION_ID,
requestConfirmations: 3,
callbackGasLimit: 500_000,
numWords: uint32(numWinners),
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
lottery.vrfRequestId = requestId;
vrfToLottery[requestId] = lotteryId;
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
internal override
{
uint256 lotteryId = vrfToLottery[requestId];
Lottery storage lottery = lotteries[lotteryId];
uint256 participantCount = lottery.participants.length;
address[] memory winners = new address[](randomWords.length);
bool[] memory isSelected = new bool[](participantCount);
for (uint256 i = 0; i < randomWords.length; i++) {
uint256 idx = randomWords[i] % participantCount;
// Если участник уже выбран — ищем следующего
while (isSelected[idx]) {
idx = (idx + 1) % participantCount;
}
isSelected[idx] = true;
winners[i] = lottery.participants[idx];
// Выплачиваем приз
uint256 prize = (lottery.prizePool * lottery.prizeDistribution[i]) / 10000;
payable(winners[i]).transfer(prize);
emit WinnerPaid(lotteryId, winners[i], i + 1, prize);
}
lottery.status = LotteryStatus.CLOSED;
emit LotteryDrawn(lotteryId, winners);
}
}
No-loss lottery (PoolTogether модель)
contract NoLossLottery {
IERC20 public depositToken; // USDC
IAAVE public aavePool; // AAVE lending pool
IERC20 public aToken; // aUSDC (yield bearing)
mapping(address => uint256) public deposits;
uint256 public totalDeposited;
function deposit(uint256 amount) external {
depositToken.transferFrom(msg.sender, address(this), amount);
// Депозит в AAVE для yield
depositToken.approve(address(aavePool), amount);
aavePool.deposit(address(depositToken), amount, address(this), 0);
deposits[msg.sender] += amount;
totalDeposited += amount;
emit Deposited(msg.sender, amount);
}
function triggerDraw() external {
// Общая стоимость aTokens > totalDeposited = накопленный yield
uint256 totalWithYield = aToken.balanceOf(address(this));
uint256 yieldEarned = totalWithYield - totalDeposited;
require(yieldEarned > MIN_PRIZE, "Not enough yield");
// Запрашиваем VRF для выбора победителя
_requestRandomWinner(yieldEarned);
}
// Withdraw: пользователь получает полный депозит назад
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "Insufficient balance");
deposits[msg.sender] -= amount;
totalDeposited -= amount;
// Вывод из AAVE
aavePool.withdraw(address(depositToken), amount, msg.sender);
emit Withdrawn(msg.sender, amount);
}
}
Chainlink Automation для автоматического розыгрыша
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
contract AutoLottery is BlockchainLottery, AutomationCompatibleInterface {
// Chainlink Automation вызывает checkUpkeep каждый блок
function checkUpkeep(bytes calldata) external view override
returns (bool upkeepNeeded, bytes memory performData)
{
for (uint256 i = 1; i <= lotteryCount; i++) {
Lottery storage lottery = lotteries[i];
if (
lottery.status == LotteryStatus.OPEN &&
block.timestamp >= lottery.endTime &&
lottery.ticketsSold > 0
) {
return (true, abi.encode(i));
}
}
return (false, "");
}
// Автоматически вызывается когда upkeepNeeded = true
function performUpkeep(bytes calldata performData) external override {
uint256 lotteryId = abi.decode(performData, (uint256));
drawWinners(lotteryId);
}
}
Разработка базовой раффл лотереи — 2-3 недели. No-loss lottery с AAVE интеграцией — 4-5 недель. Chainlink Automation для автоматизации — добавить 1 неделю.







