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

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка контрактів лотерей на блокчейні
Середній
~3-5 днів
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1308
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1221
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    921
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1149
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    612
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    886

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

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

Чому on-chain randomness не працює без VRF

block.prevrandao в Ethereum після The Merge надає 1 біт впливу валідатора (включити або не включати блок). Це краще за 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 (трансфер 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 завжди в межах [0, participants.length).

Для тестнета — Sepolia з реальним VRF Coordinator. Потрібен LINK на тестнете (faucet.chain.link) та підписка на vrf.chain.link.

Сроки

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

Вартість розраховується індивідуально після уточнення механіки розіграшу та вимог до токеноміки.