Разработка смарт-контракта provably fair (доказуемая честность)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка смарт-контракта provably fair (доказуемая честность)
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка смарт-контракта provably fair (доказуемая честность)

Основная проблема рандома в блокчейне — детерминированная среда. Все узлы сети должны прийти к одному результату, значит рандом должен быть предсказуем для всех участников постфактум. Но если он предсказуем постфактум — майнер или оператор узла может предсказать его заранее. Именно поэтому block.prevrandao (ранее block.difficulty), block.timestamp и blockhash() не являются безопасным источником рандома для ставок.

Реальный случай: lottery-контракт использовал blockhash(block.number - 1) как seed. Майнер, который производил выигрышный блок, мог просто не публиковать блок и попробовать снова — пока blockhash не даст выигрышный результат. Это называется block withholding атакой.

Chainlink VRF v2.5: как это работает

Chainlink VRF (Verifiable Random Function) — криптографически верифицируемый рандом. Контракт запрашивает рандом, Chainlink oracle генерирует его вместе с криптографическим доказательством, proof верифицируется в смарт-контракте перед использованием результата. Если proof не проходит верификацию — транзакция reverts.

Ключевой момент: oracle не может предсказать, какой рандом он сгенерирует для запроса, потому что seed включает будущий blockhash, который oracle не знает в момент запроса. Это cryptographic commitment к будущему.

Интеграция VRF v2.5

VRF v2.5 поддерживает два режима оплаты: через subscription (предпополненный баланс LINK) и native token (оплата ETH/MATIC на лету). Subscription предпочтителен для высокочастотных запросов.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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 ProvablyFairLottery is VRFConsumerBaseV2Plus {
    uint256 public s_subscriptionId;
    bytes32 public keyHash; // gas lane
    uint32 public callbackGasLimit = 100000;
    uint16 public requestConfirmations = 3; // минимум 3 блока ожидания
    
    mapping(uint256 => address) private requestToPlayer;
    mapping(uint256 => uint256) private requestToGameId;
    
    event RandomnessRequested(uint256 requestId, address player, uint256 gameId);
    event GameResolved(uint256 gameId, address player, uint256 randomWord, bool won);
    
    function requestRandomness(uint256 gameId) external returns (uint256 requestId) {
        requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: keyHash,
                subId: s_subscriptionId,
                requestConfirmations: requestConfirmations,
                callbackGasLimit: callbackGasLimit,
                numWords: 1,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
        requestToPlayer[requestId] = msg.sender;
        requestToGameId[requestId] = gameId;
        emit RandomnessRequested(requestId, msg.sender, gameId);
    }
    
    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
        address player = requestToPlayer[requestId];
        uint256 gameId = requestToGameId[requestId];
        
        // Используем modulo для получения числа в диапазоне
        // Важно: modulo bias существует для не-степеней-двойки, но для игровых целей приемлем
        uint256 result = randomWords[0] % 100; // 0-99
        bool won = result < 40; // 40% шанс выигрыша
        
        // Effects перед interactions
        delete requestToPlayer[requestId];
        delete requestToGameId[requestId];
        
        if (won) {
            _sendPrize(player, gameId);
        }
        
        emit GameResolved(gameId, player, randomWords[0], won);
    }
}

Почему requestConfirmations важен

3 блока подтверждения означает, что callback придёт через ~36 секунд на Ethereum. Это не баг, это защита: oracle не может знать blockhash для блока, который ещё не добыт. 1 подтверждение даёт меньше гарантий, потому что reorg на 1 блок возможен. Для high-stakes игр рекомендуем 5-7 подтверждений.

Commit-Reveal: когда VRF избыточен

Для сценариев, где не нужна немедленная верификация, commit-reveal схема работает без внешних oracle и бесплатна в части инфраструктуры.

Схема:

  1. Игрок в транзакции отправляет hash(secret + nonce) — commitment
  2. Оператор (или другой пользователь) раскрывает свой secret в следующем блоке
  3. Рандом = keccak256(playerSecret XOR operatorSecret XOR blockhash)

Уязвимость классического commit-reveal: оператор видит секрет игрока до reveal и может решить не раскрывать свой секрет (griefing). Защита: таймаут с penalization — если оператор не раскрывает в течение N блоков, он теряет депозит, а игрок получает refund.

Commit-reveal подходит для: рандомизации порядка mint в коллекции NFT post-reveal, выбора победителей раffle с небольшими ставками, игр где обе стороны мотивированы завершить раунд.

Верификация честности на frontend

Provably fair без возможности верификации пользователем — это просто маркетинг. Реализуем полный цикл верификации:

// Пользователь может самостоятельно проверить результат
async function verifyGameResult(gameId: string) {
  const events = await contract.queryFilter(
    contract.filters.GameResolved(gameId)
  );
  const { randomWord, requestId } = events[0].args;
  
  // Получаем proof из Chainlink
  const proofData = await fetchChainlinkVRFProof(requestId);
  
  // Верифицируем локально
  const isValid = verifyVRFProof(proofData.proof, proofData.publicKey, randomWord);
  
  return {
    gameId,
    randomWord: randomWord.toString(),
    result: randomWord.mod(100).toNumber(),
    proofValid: isValid,
    txHash: events[0].transactionHash,
  };
}

Аудит provably fair контрактов

Специфические векторы атак, которые проверяем:

Front-running перед reveal. Если результат можно предсказать на основании pending транзакции (commit-reveal схема) — атакующий может успеть поставить на выигрышный исход. Защита: commitment должен быть зафиксирован до того, как игрок знает seed оператора.

Replay attack на requestId. Что происходит, если callback вызывается дважды для одного requestId? Контракт должен отмечать fulfilled requests и reject повторный вызов.

Griefing через невыполненные requests. Если игрок создал много незавершённых VRF запросов (не дождался callback), это может блокировать логику контракта, завязанную на pending state. Ограничиваем количество активных requests на адрес.

Зависимость результата от gas price. Некоторые контракты используют gasleft() или tx.gasprice как дополнительный entropy. Это делает результат предсказуемым для MEV-ботов.

Разработка провабли-фэйр контракта с Chainlink VRF: 3-5 рабочих дней. Стоимость рассчитывается индивидуально.