Разработка системы ставок на события на блокчейне
Polymarket обрабатывает сотни миллионов долларов в объёме ставок на политические события — и делает это через Polygon с USDC в качестве расчётной валюты. Вся система ставок — это смарт-контракты с условным исходом. Ключевой вопрос при разработке любой подобной системы не «как принять ставку», а «кто и как резолвирует исход».
Оракульный вопрос — центральный инженерный вызов
Исход события «Байден выиграет выборы» нельзя получить из Chainlink Price Feed. Это не числовое значение с децентрализованным консенсусом — это суждение. Для систем предсказаний это означает один из нескольких подходов к резолюции.
UMA Optimistic Oracle
UMA использует optimistic модель: любой может предложить исход, другой участник может оспорить. Оспаривание запускает dispute resolution через UMA token holders голосование. Экономическая безопасность держится на bond: предлагающий исход вносит залог USDC или UMA, который теряет при неверной резолюции.
Интеграция с UMA:
import "@uma/core/contracts/optimistic-oracle-v3/interfaces/OptimisticOracleV3Interface.sol";
contract EventBettingMarket {
OptimisticOracleV3Interface immutable oracle;
bytes32 public assertionId;
function resolveMarket(bytes memory claim) external {
// Вносим bond, делаем assertion
assertionId = oracle.assertTruth(
claim, // "Team A won the match"
address(this), // asserter
address(0), // callbackRecipient (мы сами)
address(0), // escalationManager
7200, // challengeWindow: 2 часа
IERC20(currency),
bond,
identifier,
bytes32(0)
);
}
function assertionResolvedCallback(bytes32 _assertionId, bool assertedTruthfully) external {
require(msg.sender == address(oracle));
if (assertedTruthfully) {
_settleWinners();
} else {
_refundBettors();
}
}
}
Преимущество UMA: дёшево для большинства рынков (никакого голосования при отсутствии dispute), дорого и медленно при оспаривании.
Chainlink для числовых исходов
Когда исход числовой («цена BTC будет выше $100K на 31 декабря»), Chainlink AggregatorV3Interface — правильный выбор. Детерминированный, дёшевый, без dispute window.
function resolveNumericMarket() external {
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(updatedAt >= marketEndTime, "Price data too old");
require(block.timestamp >= marketEndTime, "Market not ended");
bool outcomeA = price >= int256(targetPrice);
_settleMarket(outcomeA);
}
Критическая проблема: Chainlink апдейтит данные по deviation threshold или heartbeat — не на конкретную секунду. Если рынок закрывается в момент между апдейтами, latestRoundData вернёт данные часовой (или более) давности. Нужно явно проверять updatedAt и иметь fallback.
Мультиоракульный подход для продакшн
Для критически важных рынков с большими объёмами — агрегация нескольких источников:
| Оракул | Тип исхода | Latency | Стоимость |
|---|---|---|---|
| Chainlink | Числовой (price) | ~1 час | Низкая |
| Pyth Network | Числовой (price) | Секунды | Низкая |
| UMA Optimistic | Произвольный | 2+ часа | Средняя |
| API3 dAPI | Числовой (первый класс данных) | Минуты | Низкая |
| Кастомный multisig | Любой | Мгновенно | 0 (доверие) |
Для спортивных результатов или политики оптимален UMA + резервный multisig (5-из-9 доверенных адресов) с timelock. Multisig только как последняя линия, если UMA dispute затянулся.
Модели распределения выигрыша
Fixed odds (букмекерская модель)
Коэффициенты фиксируются при создании рынка. Букмекер несёт риск при неправильной оценке вероятностей. Сложно реализовать on-chain без централизованной переоценки odds.
Parimutuel (тотализатор)
Все ставки попадают в пул. Выигравшие делят пул пропорционально ставкам. Коэффициент определяется итоговым распределением ставок, не известен заранее.
mapping(uint8 => uint256) public totalBets; // outcome => total ETH bet
mapping(address => mapping(uint8 => uint256)) public userBets;
function claimWinnings(uint8 outcome) external {
require(resolvedOutcome == outcome, "Wrong outcome");
uint256 userBet = userBets[msg.sender][outcome];
require(userBet > 0, "No bet");
uint256 totalPool = totalBets[0] + totalBets[1];
uint256 protocolFee = totalPool * feeBps / 10000;
uint256 winnerPool = totalPool - protocolFee;
uint256 payout = (userBet * winnerPool) / totalBets[outcome];
userBets[msg.sender][outcome] = 0;
IERC20(currency).transfer(msg.sender, payout);
}
CLOB (Central Limit Order Book) — Polymarket подход
Polymarket использует conditional tokens (ERC-1155 стандарт, Gnosis Conditional Tokens Framework). Каждый исход — это ERC-1155 токен. Рынок conditional tokens торгуется через CLOB с лимитными ордерами.
Преимущество: настоящий price discovery, ликвидность агрегируется, маркет-мейкеры могут участвовать. Сложность: нужен off-chain orderbook + on-chain settlement, matching engine.
Создание и управление рынком
Factory pattern для рынков
Каждый рынок — отдельный контракт, деплоированный через фабрику. Это изолирует риски: проблема одного рынка не влияет на другие.
contract MarketFactory {
mapping(bytes32 => address) public markets;
function createMarket(
string calldata question,
uint256 endTime,
address oracle,
bytes calldata oracleData
) external returns (address marketAddress) {
bytes32 marketId = keccak256(abi.encode(question, endTime, oracle));
require(markets[marketId] == address(0), "Market exists");
BettingMarket market = new BettingMarket(
question, endTime, oracle, oracleData, feeBps
);
markets[marketId] = address(market);
emit MarketCreated(marketId, address(market), question, endTime);
return address(market);
}
}
Emergency pause и refund
Если оракул не может резолвировать исход (событие отменено, данные недоступны), должен быть emergency механизм для возврата ставок. Emergency admin (multisig) вызывает cancelMarket(), все участники могут забрать депозиты. Timelock на отмену — минимум 24 часа, чтобы предотвратить злоупотребления со стороны admin.
Compliance и front-running защита
Commit-reveal scheme для больших ставок: пользователь сначала отправляет commit(keccak256(amount, secret)), через N блоков — reveal(amount, secret). Это предотвращает front-running на популярных исходах перед закрытием рынка.
Для спортивных ставок — временная блокировка за 5 минут до старта события. После старта новые ставки не принимаются. Реализуется через endBettingTime = eventStartTime - 5 minutes.
Стек разработки
Контракты: Solidity 0.8.24 + OpenZeppelin (AccessControl, Pausable, ReentrancyGuard) + Foundry для тестирования. Тесты включают fork-тест с реальным UMA и Chainlink на Polygon mainnet.
Для conditional tokens (если выбрана Polymarket-like архитектура): Gnosis Conditional Tokens Framework (github.com/gnosis/conditional-tokens-contracts) как базовая библиотека.
Off-chain: The Graph субграф для индексирования рыночных событий, ставок, резолюций. GraphQL API для фронтенда.
Процесс работы
Аналитика (2–3 дня). Тип событий, модель распределения выигрыша, выбор оракула, compliance требования, целевые чейны.
Проектирование (3–5 дней). Схема контрактов, оракульная интеграция, механизм резолюции, emergency процедуры.
Разработка (3–8 недель). В зависимости от сложности — от parimutuel MVP до полноценного CLOB с conditional tokens.
Аудит. Ставочные контракты — высокоприоритетный аудит из-за прямого управления деньгами пользователей и сложной логики резолюции.
Ориентиры по срокам
Простой parimutuel рынок с Chainlink оракулом — 1–2 недели. Система с UMA oracle, factory pattern, множеством рынков, The Graph индексацией — 6–10 недель. CLOB с conditional tokens и маркет-мейкинг инфраструктурой — 3–5 месяцев.







