Розробка 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 тижнів
Вартість розраховується індивідуально після оцінки ваших вимог. Зв'яжіться з нами для консультації — проаналізуємо ваш проект та запропонуємо оптимальне рішення. Замовте розробку під ключ і отримайте готовий продукт з гарантією якості.







