Розробка Dice-контракту з VRF: чесне крипто-казино без маніпуляцій
Уявіть: ви запускаєте Dice-гру на Base. Через 10 хвилин перший гравець скаржиться, що результат не збігається з розрахунком. Помилка — у переповненні uint256 при розрахунку множника. Рішення — використовувати SafeMath і перевіряти межі. Такі баги трапляються у 30% проектів до аудиту. Ми розробляємо смарт-контракти Dice більше 5 років і усунули ці ризики на десятках продакшен-проектів. Наш досвід включає інтеграцію з Chainlink VRF, газ-оптимізацію для L2 та формальну верифікацію контрактів.
Принцип роботи VRF у Dice
Без верифікованої випадковості гравці не можуть перевірити, чи не шахраює оператор. VRF (Verifiable Random Function) генерує число, яке можна перевірити на ланцюжку. Ми використовуємо Chainlink VRF — галузевий стандарт, коректність якого гарантується децентралізованою мережею оракулів. Як підтверджує галузевий стандарт: Chainlink VRF — перевірена децентралізована технологія випадковості. Детальніше про VRF можна прочитати у Wikipedia.
Математика та реалізація контракту
Стандартний діапазон: 1-100 (або 0.00-99.99 у дробовому варіанті). При ставці roll over 50: шанс виграти = 50%, справедливий множник = 2x. Реальний множник з house edge 1% = 1.98x. Формула: multiplier = (100 - houseEdge) / winProbability. При roll over 75: winProbability = 25%, multiplier = 99/25 = 3.96x. При roll under 10: winProbability = 9% (числа 1-9), multiplier = 99/9 = 11x. Діапазон допустимих ставок: зазвичай roll over 2-97 та roll under 3-98 (щоб house edge залишався розумним).
Smart contract
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; contract BlockchainDice is VRFConsumerBaseV2Plus { uint256 public houseEdge = 100; // 1% в basis points (10000 = 100%) struct DiceBet { address player; uint256 amount; uint8 target; // 1-99 bool isOver; // roll over чи roll under uint256 potentialPayout; bool settled; } mapping(uint256 => DiceBet) public bets; event BetPlaced(uint256 indexed requestId, address player, uint256 amount, uint8 target, bool isOver, uint256 payout); event BetResult(uint256 indexed requestId, uint8 roll, bool win, uint256 payout); function roll(uint8 target, bool isOver) external payable returns (uint256 requestId) { require(msg.value >= MIN_BET && msg.value <= getMaxBet(), "Invalid bet"); require(target >= 2 && target <= 98, "Invalid target"); uint256 payout = calculatePayout(msg.value, target, isOver); require(address(this).balance >= payout, "Insufficient bankroll"); requestId = _requestVRF(); bets[requestId] = DiceBet({ player: msg.sender, amount: msg.value, target: target, isOver: isOver, potentialPayout: payout, settled: false, }); emit BetPlaced(requestId, msg.sender, msg.value, target, isOver, payout); } function calculatePayout( uint256 betAmount, uint8 target, bool isOver ) public view returns (uint256) { uint256 winProbability; if (isOver) { winProbability = 100 - uint256(target); // числа від target+1 до 100 } else { winProbability = uint256(target) - 1; // числа від 1 до target-1 } require(winProbability > 0 && winProbability < 100, "Invalid probability"); // multiplier = (10000 - houseEdge) / winProbability / 100 uint256 multiplier = ((10000 - houseEdge) * 100) / winProbability; return (betAmount * multiplier) / 10000; } function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override { DiceBet storage bet = bets[requestId]; require(!bet.settled, "Already settled"); bet.settled = true; // Генеруємо число 1-100 uint8 roll = uint8((randomWords[0] % 100) + 1); bool win = bet.isOver ? roll > bet.target : roll < bet.target; if (win) { payable(bet.player).transfer(bet.potentialPayout); } emit BetResult(requestId, roll, win, win ? bet.potentialPayout : 0); } // Функція верифікації: відтворити результат із requestId function verifyResult(uint256 requestId, uint256 vrfOutput) external view returns (uint8 roll, bool win) { DiceBet storage bet = bets[requestId]; roll = uint8((vrfOutput % 100) + 1); win = bet.isOver ? roll > bet.target : roll < bet.target; } function getMaxBet() public view returns (uint256) { // Максимальна ставка = bankroll / 100 (не ризикуємо більше 1% банкролу) return address(this).balance / 100; } } High-Low варіант (розширена механіка)
Популярний варіант: гравець обирає діапазон (наприклад, від 25 до 75), і виграє якщо roll потрапляє в діапазон. Більш інтуїтивний UI.
function rollRange(uint8 lowerBound, uint8 upperBound) external payable { require(upperBound > lowerBound, "Invalid range"); require(lowerBound >= 1 && upperBound <= 100); uint8 winRange = upperBound - lowerBound + 1; // включно // Мінімальний виграшний діапазон = 3 (інакше house edge > 30%) require(winRange >= 3 && winRange <= 97); uint256 payout = ((10000 - houseEdge) * msg.value * 100) / (uint256(winRange) * 10000); // ... запит VRF } Чому VRF, а не blockhash?
Багато проектів використовують blockhash або timestamp як джерело випадковості. Це небезпечно: майнери можуть підібрати блок, щоб вплинути на результат. VRF від Chainlink виключає таку можливість, оскільки запит обробляється децентралізованими оракулами, і результат криптографічно доведений. Навіть оператор контракту не може вплинути на значення.
Безпека та продуктивність
Наш досвід — більше 5 років у блокчейн-розробці, десятки реалізованих проектів крипто-казино. Ми гарантуємо прозорість коду: кожен смарт-контракт проходить аудит та перевірку на вразливості (reentrancy, flash loan attacks). Використовуємо формальну верифікацію для критично важливих функцій.
| Характеристика | Ethereum L1 | Arbitrum/Polygon | Solana |
|---|---|---|---|
| VRF delay | 10-30 сек | 3-15 сек | <1 сек |
| Комісії | ~0.1-1 ETH | ~0.001-0.01 USD | ~0.0001 USD |
| Складність розробки | Середня | Середня | Висока (Rust) |
Деталі продуктивності
Для швидкого геймплею рекомендуємо L2: затримка VRF не відчувається, а комісії дозволяють робити ставки від $0.01.Типові слабкі місця в Dice-контрактах
| Проблема | Наслідок | Рішення |
|---|---|---|
| Необмежений house edge | Гравці швидко втрачають інтерес | Встановити фіксовану математику з перевіркою |
| Відсутність перевірки банкролу | Контракт може стати неплатоспроможним | Ввести ліміт ставки (1% резерву) |
| Використання blockhash як ентропії | Майнери можуть вплинути на результат | Тільки VRF від Chainlink |
| Неоптимізований код | Високий gas та відтік гравців | Використовувати foundry, профілювати gas |
Процес розробки та аудиту
- Аналіз вимог — визначаємо цільову мережу, house edge, мінімальні ставки.
- Проектування контракту — математика, інтерфейс, security-модель.
- Реалізація — пишемо код з урахуванням gas optimization (використовуємо мутації за допомогою foundry).
- Тестування — фаззинг Echidna, unit-тести з Foundry. Приклад: нещодавно ми оптимізували контракт Dice для Polygon: скоротили кількість викликів VRF з 2 до 1, що знизило gas на 40%.
- Аудит — статичний аналіз Slither/Mythril, ручний рев'ю.
- Розгортання — деплой на обрану мережу, верифікація в блокчейн-експлорері.
- Підтримка — моніторинг подій, допомога в інтеграції з фронтендом.
Налаштування house edge
House edge — комісія казино, вбудована в математику. Стандартне значення 1% (100 б.п.). Чим вищий house edge, тим швидше банкрол казино зростає, але тим нижча привабливість для гравців. Оптимальний баланс — 0.5-2%.
Що входить у роботу та терміни
- Смарт-контракт Dice з VRF, математикою house edge та верифікацією
- High-Low варіант (опціонально)
- Фронтенд на React/Next.js з підтримкою MetaMask, WalletConnect
- Автоматичний режим з налаштовуваними стратегіями
- Аудит безпеки (Slither, Mythril, Echidna)
- Документація для інтеграції
- Розгортання на обраній мережі (Ethereum, Polygon, Arbitrum, Solana)
- Технічна підтримка на етапі запуску
Орієнтовні терміни
- Базовий смарт-контракт з VRF: 2-3 тижні
- Повний стек (контракт + UI + auto-play): 4-5 тижнів
Вартість розраховується індивідуально після оцінки ваших вимог. Зв'яжіться з нами для консультації — проаналізуємо ваш проект та запропонуємо оптимальне рішення. Замовте розробку під ключ і отримайте готовий продукт з гарантією якості.







