Розробка механіки Reveal для NFT
Reveal — це момент, коли NFT «відкривається»: зображення-заглушка замінюється фінальним. Технічно це означає оновлення baseURI в контракті або перемикання логіки tokenURI. Проблема не в реалізації самого reveal—проблема у справедливості: хто знав фінальне відображення tokenId → trait до reveal, і чи міг це використати для снайперства рідкісних токенів?
Чому Random Reveals — це завдання безпеки
Проблема передбачуваності
Якщо команда проекту зберігає файли метаданих заздалегідь (наприклад, 1.json, 2.json, ..., 10000.json), вони знають ще до публічного reveal, який tokenId отримає який trait. Простий сценарій: команда робить мінтинг під час presale, знаючи, що токен #777 — легендарний. Або гірше — продають цю інформацію.
Проблема існує навіть якщо команда чесна: файли метаданих часто завантажуються на IPFS до запуску. Уважний дослідник може виявити CID, отримати доступ до метаданих через gateway та знайти рідкісні токени на вторинному ринку одразу після 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. Схема:
- Контракт запитує випадковість через
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 не виконається, випадковість буде втрачена. Для простого reveal достатньо 100k gas.
Альтернативні підходи
Commit-reveal від команди. Команда публікує хеш keccak256(secret) до мінтингу та розкриває secret після закінчення мінтингу. Offset = uint256(keccak256(secret)) % totalSupply. Справедливо, якщо команда не може змінити secret після публікації хешу. Недолік — припущення довіри до команди.
Затримана завантаження метаданих. Контракт розгортається без baseURI. Після закінчення мінтингу команда генерує відображення, завантажує на IPFS та встановлює baseURI. Технічно справедливо, але непрозоро для користувачів — немає on-chain гарантії.
Провенанс хеш. Стандарт, популяризований Bored Ape Yacht Club: до розгортання публікується хеш конкатенації всіх зображень у фінальному порядку. Користувачі можуть переконатись, що зображення не змінилися після публікації хешу. Не вирішує проблему передбачуваності призначення, але фіксує контент.
Реалізація в контракті
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 запит
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 день). Реєстрація підписки на vrf.chain.link, поповнення LINK, додавання контракту consumer.
Розробка та тестування (1-2 дні). Контракт з інтеграцією VRF. Foundry mock для локального тестування fulfillRandomWords—використовуємо VRFCoordinatorV2Mock з бібліотеки Chainlink.
Тестування на Sepolia (1 день). VRF працює на всіх основних testnets. Перевіряємо повний flow: мінт → requestReveal → очікування callback → перевірка tokenURI.
Оцінка часу
Механіка reveal як окремий компонент для існуючого контракту — 2-4 дні. Як частина повної розробки колекції — включено у основний обсяг.







