Представьте: вы запускаете лотерейный dApp с пулом $100K. Участники требуют гарантий честности — выбор победителя через block.timestamp даёт валидатору возможность манипуляции. Один неверный шаг — и средства могут быть потеряны из-за reentrancy или front-running. Мы, блокчейн-инженеры с опытом в десятках проектов, решаем эти проблемы внедрением Chainlink VRF и строгим аудитом контрактов. Подобная архитектура уже использовалась в лотереях с совокупным пулом более $2M — ни одной уязвимости. В этой статье расскажем, как построить верифицируемо честную лотерею на смарт-контрактах.
Почему on-chain randomness небезопасна?
block.prevrandao в Ethereum даёт валидатору 1 бит влияния. RANDAO — агрегированная entropy, но последний reveal имеет влияние. Для лотереи с пулом >$1M это экономически атакуемо: валидатор может скрыть reveal. Chainlink VRF решает проблему криптографически: случайное генерируется off-chain с доказательством, проверяемым on-chain. Подделать random невозможно. Более того, VRF использует пару ключей: секретный ключ оракула генерирует число, а public key позволяет контракту проверить доказательство. Это гарантирует честность даже при недоверии к оператору VRF.
Как Chainlink VRF обеспечивает честность?
Контракт запрашивает случайное число через requestRandomWords, а оракул возвращает его в fulfillRandomWords вместе с доказательством. Контракт проверяет доказательство — если оно невалидно, результат отклоняется. Мы используем subscription-модель VRF 2.5, которая дешевле при частых запросах: платите один раз за подписку и затем только за газ. Газовые затраты на один запрос — около 200k газа, что на Ethereum эквивалентно примерно $5-10 при цене газа 20 Gwei. Для лотерей с частыми розыгрышами это приемлемо.
Архитектура лотерейного контракта с 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, рекомендовано от 5. callbackGasLimit: лимит газа для fulfillRandomWords. Если логика тратит больше газа — транзакция упадёт, контракт зависнет. Лучше хранить winnerIndex и дать победителю claim приз. Subscription vs Direct Funding: subscription модель рекомендована для регулярных розыгрышей — она снижает стоимость запросов до 40%.
Как защититься от front-running?
Если момент розыгрыша известен, MEV-боты могут купить последний билет в одном блоке с drawWinner. Решения: commit-reveal для покупки билетов или закрытие продаж за N блоков до розыгрыша. Chainlink Automation устраняет ручной вызов — контракт сам вызывает drawWinner по расписанию или при выполнении условий. Это делает атаку front-running практически невозможной.
Какие уязвимости типичны для лотерейных контрактов?
Reentrancy при выплате
Используем pull-паттерн: победитель сам вызывает claimPrize(), в котором обновляем состояние до перевода. Это исключает reentrancy. В нашем коде выше используется push-перевод — для production-контракта мы всегда заменяем его на pull.
Централизация управления
onlyOwner на drawWinner — централизация. Автоматизируем розыгрыш через Chainlink Automation, что снимает риск колюзий владельца с участниками.
Интеграция с Chainlink Automation
Розыгрыш по расписанию или по условию без ручного вызова:
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 {
drawWinner();
}
Тестирование и аудит
Тесты на Foundry с mock VRF Coordinator. Покрытие кода — 100% ветвлений, 99% строк. Fuzzing на параметры (количество участников, суммы, gas limit). Для тестнета — Sepolia с реальным VRF. Мы гарантируем качество: более 50 реализованных проектов, 15+ лотерейных систем.
Что входит в работу
- Разработка смарт-контракта с VRF и Automation
- Покрытие тестами (Foundry, fuzzing)
- Деплой на mainnet/testnet
- Документация и инструкция по эксплуатации
- Поддержка после запуска (2 недели)
| Метод выплаты | Безопасность | Газовые затраты | Дополнительные риски |
|---|---|---|---|
| Push (прямой перевод) | Низкая (reentrancy) | Низкие | Reentrancy, высокая стоимость при ошибке |
| Pull (claim) | Высокая | Средние (победитель платит) | Зависимость от пользователя |
| Источник случайности | Безопасность | Стоимость | Пример |
|---|---|---|---|
| block.timestamp | Низкая (атака майнера) | Бесплатно | Любительский контракт |
| block.prevrandao | Средняя (1 бит влияния) | Бесплатно | Старые проекты |
| Chainlink VRF | Криптографическая | LINK за запрос | Надёжная лотерея |
Сроки
Базовый контракт: 3-5 дней разработки + 1-2 дня тестирования. Расширенный (много пулов, NFT-билеты): 2-3 недели. Аудит рекомендуется для любого контракта с пулом >$50K. Стоимость рассчитывается индивидуально.
Свяжитесь с нами для консультации по вашему проекту — проанализируем требования и предложим оптимальное решение. Закажите разработку лотерейного контракта под ключ с гарантией честности.







