Разработка reveal-механики NFT
Reveal — момент, когда NFT «открывается»: placeholder изображение заменяется финальным. Технически это обновление baseURI в контракте или переключение логики tokenURI. Проблема не в реализации reveal как такового — проблема в честности: кто знал финальный mapping tokenId → trait до открытия, и мог ли он это использовать для снайпинга редких токенов.
Почему случайность reveal — это задача безопасности
Проблема предсказуемости
Если команда проекта хранит файлы метаданных заранее (например, 1.json, 2.json, ..., 10000.json), то до публичного reveal они знают, какой tokenId получает какой trait. Простой сценарий: команда минтит во время presale, зная, что токен #777 — легендарный. Или — хуже — продаёт эту информацию.
Проблема существует даже если команда честная: файлы метаданных часто загружаются на IPFS до launch. Внимательный исследователь может обнаружить CID, получить доступ к метаданным через gateway и снайпить редкие токены на aftermarket сразу после reveal.
Уязвимость on-chain randomness
Ранние проекты использовали blockhash(block.number - 1) или keccak256(abi.encodePacked(block.timestamp, msg.sender)) как источник случайности для reveal. Обе реализации предсказуемы. Майнер (на PoW) или валидатор (на PoS) контролирует block.timestamp в пределах нескольких секунд. Атакующий контракт может проверить, какой trait он получит, и revert если невыгодно — это называется reroll attack.
Любой on-chain источник случайности уязвим, потому что результат детерминирован и виден заранее тому, кто строит блок.
Chainlink VRF как стандарт честного reveal
Chainlink VRF (Verifiable Random Function) v2 — единственный production-ready способ получить верифицируемо случайное число on-chain. Схема:
- Контракт запрашивает randomness через
requestRandomWords(keyHash, subId, confirmations, callbackGasLimit, numWords) - Chainlink oracle генерирует случайное число + криптографическое доказательство
- Через 1-3 блока вызывается
fulfillRandomWords(requestId, randomWords)в нашем контракте - Контракт сохраняет
revealOffset = randomWords[0] % totalSupply
После reveal: tokenURI(tokenId) возвращает метаданные для (tokenId + revealOffset) % totalSupply. Команда не знает revealOffset до получения ответа от VRF — честность гарантирована криптографически.
Subscription vs Direct Funding
VRF v2 поддерживает две модели оплаты. Subscription — пополняем баланс в LINK на subscriptionId, несколько контрактов могут использовать одну подписку. Direct Funding — каждый запрос оплачивается отдельно. Для reveal используем Subscription: один запрос на весь проект, стоимость 0.1-0.2 LINK (Ethereum) или меньше на L2.
Важно: callbackGasLimit должен покрывать выполнение fulfillRandomWords. Если газ слишком низкий — callback не выполнится, randomness будет потеряна. Для simple reveal достаточно 100k gas.
Альтернативные подходы
Commit-reveal от команды. Команда публикует хэш keccak256(secret) перед минтингом, после окончания раскрывает secret. Offset = uint256(keccak256(secret)) % totalSupply. Честно, если команда не может изменить secret после публикации хэша. Минус — trust assumption на команду.
Delayed upload метаданных. Контракт деплоится без baseURI. После окончания минта команда генерирует mapping, загружает на IPFS, устанавливает baseURI. Технически честно, но непрозрачно для пользователей — нет on-chain гарантии.
Provenance hash. Стандарт, popularized Bored Ape Yacht Club: до деплоя публикуется хэш от concatenation всех изображений в финальном порядке. Пользователи могут верифицировать, что изображения не менялись после публикации хэша. Не решает проблему предсказуемости assignment, но фиксирует контент.
Реализация в контракте
uint256 public revealOffset;
bool public revealed;
string public unrevealedURI;
function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (!revealed) return unrevealedURI;
uint256 revealedId = (tokenId + revealOffset) % totalSupply();
return string(abi.encodePacked(baseURI, revealedId.toString(), ".json"));
}
// Вызывается только один раз после окончания минта
function requestReveal() external onlyOwner {
require(!revealed, "Already revealed");
// Chainlink VRF v2 request
COORDINATOR.requestRandomWords(keyHash, subId, 3, 100000, 1);
}
function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override {
revealOffset = randomWords[0] % totalSupply();
revealed = true;
emit Revealed(revealOffset);
}
Процесс работы
Конфигурация VRF (1 день). Регистрация subscription на vrf.chain.link, пополнение LINK, добавление consumer контракта.
Разработка и тесты (1-2 дня). Контракт с VRF интеграцией. Foundry mock для тестирования fulfillRandomWords локально — используем VRFCoordinatorV2Mock из Chainlink library.
Тестирование на Sepolia (1 день). VRF работает на всех основных testnet. Верифицируем полный flow: минт → requestReveal → ожидание callback → проверка tokenURI.
Ориентиры по срокам
Reveal механика как отдельный компонент к существующему контракту — 2-4 дня. В составе полной разработки коллекции — входит в основной scope.







