Como o Chainlink VRF v2.5 é usado pelo BitChickenForge para randomizar o gacha de ovos,
e como provisionar/operar o VRF em localnet, testnet e mainnet.
O gacha segue o padrão de comprometimento-e-revelação do Chainlink VRF para garantir que nenhuma parte (nem o comprador, nem o operador) possa prever ou manipular o resultado antes da execução.
Comprador BitChickenForge VRF Coordinator BitChickenNFT
| | | |
|-- requestObtain(tier, ...) -->| | |
| (msg.value = tierPrice) |-- requestRandomWords(...) -->| |
| | (subId, keyHash, gas) | |
| |<-- requestId ----------------| (N blocos depois) |
| | | |
| |<-- fulfillRandomWords -------| |
| | (requestId, [randomWord]) | |
| |-- nft.pickEdition(tier, w) ->| |
| |<-- editionId ----------------| |
| |-- nft.forgeMint(...) ------->| |
| |<-- (tokenId, referrer, bps) -| |
| |-- emit ForgeFulfilled -------> |
requestObtainChamada pelo comprador com msg.value == nft.tierPrice(tier_).
msg.value é exato (IncorrectPayment) e que há edição disponível para o tier (NothingAvailable).ForgeRequest { buyer, tier, referrerCode, name, paid, blockNumber } em requests[requestId].msg.value em totalPendingRefunds — withdraw() nunca drena esse escrow.s_vrfCoordinator.requestRandomWords(...) e recebe de volta o requestId.ForgeRequested(buyer, requestId, tier).| Parâmetro | Variável no contrato | Tipo | Descrição |
|---|---|---|---|
keyHash |
bytes32 public keyHash |
gas lane | Identifica o job do Chainlink (latência e custo) |
subId |
uint256 public subId |
subscription ID | Subscription que paga os pedidos de VRF em LINK |
callbackGasLimit |
uint32 public callbackGasLimit |
wei de gás | Limite de gás para a execução do callback; válido entre 50_000 e 2_500_000 |
requestConfirmations |
uint16 public requestConfirmations |
blocos | Mínimo de confirmações antes do VRF responder (>= 1) |
numWords |
fixo em 1 no código |
— | Apenas 1 palavra aleatória é pedida por ovo |
Os parâmetros são definidos no construtor e atualizáveis via setVRFConfig(...) (apenas owner).
fulfillRandomWords (callback)Chamado exclusivamente pelo VRF coordinator (protegido em VRFConsumerBaseV2Plus).
requests[requestId] (CEI — estado primeiro).gender = uint8(randomWord & 1) (bit menos significativo).nft.pickEdition(tier, randomWord) — seleção ponderada cumulativa entre edições elegíveis.nft.forgeMint(buyer, editionId, gender, name, referrerCode).totalPendingRefunds -= paid (BNB torna-se receita).pendingReferralBnb[referrer] += reward.ForgeFulfilled(buyer, requestId, tokenId, editionId).pickEdition ou forgeMint (ex.: edição esgotada no momento do callback):
pendingRefund[buyer] += paid (BNB permanece reservado, apenas move de escrow para pull-refund do comprador).RequestCancelled(buyer, requestId, paid).No localnet (localhost / hardhat), o VRF coordinator real não existe. O deploy usa o contrato
contracts/mocks/vrf-coordinator-mock.sol, que é apenas um wrapper fino sobre
VRFCoordinatorV2_5Mock da Chainlink.
scripts/deploy.ts)1. Deployer instancia VRFCoordinatorMock(baseFee, gasPrice, weiPerUnitLink)
2. vrfMock.createSubscription() → subId
3. vrfMock.fundSubscription(subId, 10 LINK-equivalentes)
4. Forge.deploy(vrfMockAddress, nftAddress, keyHash, subId, callbackGasLimit, confirmations, ADMIN_WALLET)
5. vrfMock.addConsumer(subId, forgeAddress)
6. nft.setForge(forgeAddress)
Tudo isso é executado automaticamente por npm run deploy:localhost.
npm run forge:watchNo localnet, o mock não responde automaticamente — alguém precisa chamar
vrfMock.fulfillRandomWords(requestId, forgeAddress) para que o callback ocorra.
O script scripts/forge-watch.ts faz exatamente isso: fica em polling (setInterval 1500 ms),
consulta eventos ForgeRequested novos e chama fulfillRandomWords no mock para cada requestId
ainda não processado.
forge:watch liga → polling de eventos ForgeRequested → fulfillRandomWords no mock → ForgeFulfilled
Sem forge:watch rodando, o ovo fica travado em “Chocando…” indefinidamente. O AppHost
(Aspire) sobe esse processo automaticamente; ao rodar só a chain manualmente, execute:
npm run forge:watch
O script lê os endereços de scripts/deployed-localhost.json (gerado pelo deploy).
Acesse vrf.chain.link e conecte a carteira correspondente à rede alvo
(BNB Chain Testnet → chainId 97; Mainnet → chainId 56). Clique em Create Subscription e confirme
a transação. O subscriptionId gerado é o valor que vai em VRF_SUB_ID no .env.
Na interface do VRF, na aba Fund Subscription, transfira LINK suficiente. Cada pedido de
aleatoriedade consome LINK. Recomendação inicial: pelo menos 5-10 LINK para testes, mais para
produção (varia com o preço do LINK e o callbackGasLimit configurado).
.env antes do deployVRF_COORDINATOR=<endereço do coordinator na rede>
VRF_KEY_HASH=<keyHash do gas lane desejado>
VRF_SUB_ID=<subscriptionId gerado acima>
VRF_CALLBACK_GAS_LIMIT=500000
VRF_REQUEST_CONFIRMATIONS=3
Para a BNB Smart Chain, os endereços e key hashes são publicados em docs.chain.link/vrf/v2-5/supported-networks.
Quando connection.networkName !== 'localhost', o deploy.ts usa vrfCoordinator e vrfSubId
do .env diretamente, sem criar mock. O Forge é deployado com os parâmetros reais.
npm run deploy:testnet
# ou
npm run deploy:mainnet
Após o deploy, copie o endereço do Forge de scripts/deployed-<rede>.json e, na interface da
Chainlink (ou via contrato), chame addConsumer(subId, forgeAddress). Sem esse passo, o coordinator
rejeitará os pedidos do Forge.
Use setVRFConfig (apenas owner) se precisar ajustar parâmetros pós-deploy sem redeployar o Forge
(que é não-upgradeável).
cancelStaleRequest)Se o VRF coordinator não responder após STALE_BLOCKS (= 256 blocos), o comprador pode chamar
cancelStaleRequest(requestId). O BNB pago é movido para pendingRefund[buyer] e pode ser sacado
via claimRefund().
STALE_BLOCKS = 256 blocos ≈ ~13 minutos na BNB Chain (3-second block time)
O BNB de um pedido em voo está sempre reservado em totalPendingRefunds. A função withdraw()
do Forge só drena balance - (totalPendingRefunds + totalPendingReferralBnb), garantindo que
o owner nunca possa sacar BNB que pertence a compradores ou referrers.
Para detalhes completos do modelo de escrow e dos invariantes de segurança, veja seguranca.md.
Se forgeMint reverter durante fulfillRandomWords (ex.: edição esgotada entre o pedido e o
callback), o BNB não se perde: é movido para pendingRefund[buyer] automaticamente. O comprador
pode então sacar com claimRefund().