Blockchain Lottery Development
Blockchain lottery — one of the most natural smart contract use cases: transparent prize fund storage, verifiable drawing via Chainlink VRF, automatic payouts without intermediaries. Unlike traditional lotteries — no one can manipulate results, funds cannot be stolen, payout guaranteed by code.
Lottery Types
Fixed prize (Powerball-style): fixed jackpot, player selects numbers, wins on match. Fund taken from ticket sales + operator reserve.
Prize pool lottery (Raffle): entire prize fund = sum of all bets (minus fee). More participants = bigger prize. One winner takes all or multiple prize places.
No-loss lottery (PoolTogether-style): participants deposit into yield-generating protocol (AAVE), accumulated interest goes to winner, deposits returned to all. "Play for free, risk only the yield".
Smart Contract — Raffle Lottery
// 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 for 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;
// Operator fee
uint256 public operatorFee = 300; // 3% in 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");
// Add participant amount times (weighted: more tickets = more chances)
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;
// Request N random numbers (by count of prize places)
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;
// If participant already selected — find next
while (isSelected[idx]) {
idx = (idx + 1) % participantCount;
}
isSelected[idx] = true;
winners[i] = lottery.participants[idx];
// Pay prize
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 Model)
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);
// Deposit into AAVE for 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 {
// Total aTokens value > totalDeposited = accumulated yield
uint256 totalWithYield = aToken.balanceOf(address(this));
uint256 yieldEarned = totalWithYield - totalDeposited;
require(yieldEarned > MIN_PRIZE, "Not enough yield");
// Request VRF for winner selection
_requestRandomWinner(yieldEarned);
}
// Withdraw: user gets full deposit back
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "Insufficient balance");
deposits[msg.sender] -= amount;
totalDeposited -= amount;
// Withdraw from AAVE
aavePool.withdraw(address(depositToken), amount, msg.sender);
emit Withdrawn(msg.sender, amount);
}
}
Chainlink Automation for Automatic Drawing
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
contract AutoLottery is BlockchainLottery, AutomationCompatibleInterface {
// Chainlink Automation calls checkUpkeep every block
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, "");
}
// Automatically called when upkeepNeeded = true
function performUpkeep(bytes calldata performData) external override {
uint256 lotteryId = abi.decode(performData, (uint256));
drawWinners(lotteryId);
}
}
Developing basic raffle lottery — 2-3 weeks. No-loss lottery with AAVE integration — 4-5 weeks. Chainlink Automation for automation — add 1 week.







