Разработка контрактов лотерей на блокчейне

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка контрактов лотерей на блокчейне
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка контрактов лотерей на блокчейне

Самая частая ошибка при разработке блокчейн-лотереи — использовать block.timestamp, block.prevrandao (бывший block.difficulty) или хэш блока как источник случайности. Майнер/валидатор контролирует эти значения в пределах разумного диапазона. Для ставки в $100 атака нерентабельна, для джекпота в $500K — вполне.

Почему on-chain randomness не работает без VRF

block.prevrandao в Ethereum после The Merge предоставляет 1 бит влияния валидатора (include or not include the block). Это лучше, чем block.difficulty, но всё равно не годится для лотереи. RANDAO — агрегированная entropy от всех валидаторов в эпохе, последний reveal имеет 1 бит влияния на финальное значение. Для лотереи с пулом >1M$ это экономически атакуемо: валидатор может скрыть reveal и не финализировать блок, если случайное число его не устраивает.

Chainlink VRF v2 решает проблему криптографически: случайное число генерируется off-chain с доказательством корректности (VRF proof), которое верифицируется on-chain перед использованием. Подделать random невозможно — даже для Chainlink. Это не «доверяем оракулу», это верифицируемая случайность.

Архитектура лотерейного контракта с VRF

Базовая структура: контракт принимает участников, накапливает пул, в момент розыгрыша запрашивает случайное число у Chainlink VRF, получает его в коллбэке, выбирает победителя.

// 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 Lottery is VRFConsumerBaseV2Plus {
    uint256 public subscriptionId;
    bytes32 public keyHash;
    uint32 public callbackGasLimit = 100000;
    uint16 public requestConfirmations = 3;

    address[] public participants;
    uint256 public pendingRequestId;
    LotteryState public state;

    enum LotteryState { OPEN, DRAWING, CLOSED }

    function drawWinner() external onlyOwner {
        require(state == LotteryState.OPEN, "Not open");
        require(participants.length > 0, "No participants");
        state = LotteryState.DRAWING;

        pendingRequestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: keyHash,
                subId: subscriptionId,
                requestConfirmations: requestConfirmations,
                callbackGasLimit: callbackGasLimit,
                numWords: 1,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
    }

    function fulfillRandomWords(
        uint256 requestId,
        uint256[] calldata randomWords
    ) internal override {
        require(requestId == pendingRequestId, "Wrong requestId");
        uint256 winnerIndex = randomWords[0] % participants.length;
        address winner = participants[winnerIndex];
        state = LotteryState.CLOSED;
        // выплата победителю
        payable(winner).transfer(address(this).balance);
    }
}

Критичные детали реализации

requestConfirmations — сколько блоков ждать перед генерацией random. Минимум 3, рекомендованное для mainnet — от 5. Меньше — быстрее, но в теории открывает окно для атаки через chain reorg (хотя на Ethereum с finality ~12.8 минут это практически исключено).

callbackGasLimit — лимит газа для fulfillRandomWords. Если внутри коллбэка логика тратит больше газа — транзакция от Chainlink упадёт, random будет утерян, контракт зависнет в состоянии DRAWING. Считайте газ: итерация по 1000 участников внутри коллбэка потребует ~21K gas только на чтение адресов из storage. Лучше хранить только winnerIndex в коллбэке и позволить победителю самому claim приз.

Подписка vs Direct Funding. Subscription model (рекомендована): предварительный депозит LINK, несколько контрактов используют один баланс. Direct Funding: каждый запрос VRF оплачивается из баланса самого контракта. Для лотерей с регулярными розыгрышами — subscription дешевле операционно.

Уязвимости лотерейных контрактов

Front-running розыгрыша

Если момент розыгрыша известен заранее (например, timestamp), MEV-боты могут купить последний билет в одном блоке с транзакцией drawWinner. При малом числе участников это изменяет вероятности. Решение: commit-reveal для покупки билетов, или закрытие продаж за N блоков до розыгрыша.

Reentrancy при выплате

Классика. Паттерн push (transfer ETH победителю в fulfillRandomWords) + внешний код победителя = reentrancy. Используем pull-паттерн: победитель сам вызывает claimPrize(), в котором обновляем состояние до перевода средств.

Централизация управления

onlyOwner на drawWinner — это централизация. Владелец контракта может тянуть с розыгрышем, ждать выгодного момента (хотя randomness от VRF он не контролирует). Лучший вариант: автоматизированный розыгрыш через Chainlink Automation или Gelato, без ручного вызова.

Интеграция с Chainlink Automation

Розыгрыш по расписанию или по условию (накопилось N участников) без ручного вызова:

function checkUpkeep(bytes calldata)
    external view override returns (bool upkeepNeeded, bytes memory) {
    upkeepNeeded = (
        state == LotteryState.OPEN &&
        participants.length >= minParticipants &&
        block.timestamp >= nextDrawTime
    );
}

function performUpkeep(bytes calldata) external override {
    // вызывается Chainlink Automation когда checkUpkeep == true
    drawWinner();
}

Это устраняет единую точку отказа и централизацию управления.

Тестирование и аудит

Тесты на Foundry с mock VRF Coordinator — обязательны перед деплоем. Chainlink предоставляет VRFCoordinatorV2_5Mock для локального тестирования. Fuzzing на параметры numParticipants, randomWord — проверяем, что winner всегда в bounds [0, participants.length).

Для тестнета — Sepolia с реальным VRF Coordinator. Нужен LINK на тестнете (faucet.chain.link) и подписка на vrf.chain.link.

Сроки

Базовый лотерейный контракт с VRF и Automation: 3-5 дней разработки + 1-2 дня тестирования. Контракт с расширенной токеномикой (несколько пулов, NFT-билеты, реферальная система) — 2-3 недели. Аудит рекомендован для любого контракта с пулом >$50K.

Стоимость рассчитывается индивидуально после уточнения механики розыгрыша и требований к токеномике.