# One-shot giveaway on Robinhood Chain

`Giveaway.sol` selects one address from a list fixed at deployment, using D20DAO's published Solidity consumer and `ChooseOne` mapping. It is an educational winner-selection example: no ticket sales, prize custody, eligibility checks or prize payments.

## Files and local check

Save these four files in one new directory:

- [Giveaway.sol](./Giveaway.sol) — the `OneShotGiveaway` contract.
- [package.json](./package.json) — pinned SDK 0.6.2, ethers 6.17.0 and solc 0.8.28.
- [local-check.mjs](./local-check.mjs) — compiler and local lifecycle checks.
- [MockCoordinator.sol](./MockCoordinator.sol) — deliberately unverified local test coordinator; **never deploy this mock as a randomness service**.

Use Node 22.13 or newer. From that directory:

```sh
npm install --ignore-scripts --no-audit --no-fund
npm run compile
```

The compiler uses Solidity 0.8.28, Cancun and optimizer runs 200. It verifies imported SDK source hashes against the installed `PROTOCOL-PROVENANCE.json`, checks the relevant consumer selectors against the Robinhood coordinator ABI, and writes `build/Giveaway.artifact.json` and `build/check-report.json`.

For the lifecycle checks, install [Foundry's Anvil](https://getfoundry.sh/anvil/overview/) and make `anvil` available on your PATH, then run:

```sh
npm run check
```

The script starts and stops an isolated Anvil on `127.0.0.1`, chain 31337. It uses disposable unlocked local accounts. It does not read a `.env` file, private keys or a live RPC endpoint. It does not deploy to Robinhood Chain.

## Application rules

1. Deploy `OneShotGiveaway(coordinator, fixedEntrants)` with the correct coordinator proxy and an ordered list of 1–64 unique, nonzero addresses. Its constructor commits `keccak256(abi.encode(fixedEntrants))`. There is no function to edit the list or its ordering.
2. Agree on the giveaway contract address before the draw. Anyone can fund `draw()` immediately after deployment, so deployment means the list is ready. An organizer can still deploy another contract; this example cannot enforce a social promise to recognize only one address.
3. `draw()` moves from `Ready` to `Pending` before requesting randomness. The seed is `entrantsHash`; the request uses `ChooseOne` with the fixed population. The returned ID is stored in `requestId` and never reset.
4. `D20VRFConsumer` authenticates both result and refund callbacks. A callback must match the stored ID. The result callback only stores `word` and changes state to `Drawn`. Repeating the same ID and word is harmless; a different ID or a replacement word is rejected.
5. `winnerIndex()` applies the published rejection-sampling mapping to the saved word. `winner()` returns the address at that index. Both reject reads before a successful draw. No transfers run inside the callback.

The states are `Ready = 0`, `Pending = 1`, `Drawn = 2`, `Expired = 3`. There is no cancellation or reroll function. A refunded giveaway remains finished without a winner.

## Fees and recovery

The draw sponsor forwards a buffered quote with `draw({ value })`. Use `quoteRequestFee(provider, coordinator, 100_000)` from `@d20dao/vrf-sdk` off chain: it accounts for the latest header's base fee. Do not use `quoteFee` through `eth_call` as an off-chain quote.

The consumer forwards the full value. The coordinator checks its exact current quote in the transaction, rejects underpayment and credits any excess to the sponsor (`msg.sender` of `draw()`). That sponsor is the request's fixed refund recipient. To recover surplus, the sponsor calls `withdrawRefundCredit(recipient)` on the coordinator. A contract sponsor must support making that call or receiving a native refund. Robinhood Chain fees and gas use ETH.

Use the coordinator's request state to choose recovery:

- **Accepted proof, failed callback:** anyone can call `retryCallback(requestId, gasLimit)` with sufficient gas. This delivers the same stored word and charges no second service fee; the retry transaction still costs gas. A fulfilled request is not refundable.
- **Past the deadline without an accepted proof:** anyone can call `refundRequest(requestId)`. The coordinator settles the request's snapshotted refundable fee to its fixed sponsor, directly or as backed credit. Gas is not refunded. The authenticated notification makes this giveaway `Expired`.
- **Refund settled, notification failed:** anyone can call `retryRefundCallback(requestId, gasLimit)`. This retries only notification and does not refund again. Until it succeeds, the consumer can still show `Pending`; a second draw remains impossible.

The initial callback budget is 100,000 gas. Verify current deployments and supported refund notifications using the [deployment guide](https://d20dao.org/docs/deployments) and [public SDK](https://github.com/d20dao/d20-sdk). Inspect current request/pricing state before funded actions. The included mock uses adjustable delivery gas to exercise failure paths; its testing methods are not the live coordinator API.

## Reproduce the winning index

The JavaScript and Solidity mappings use the same specification:

```js
import { builtins, mapRandomness } from '@d20dao/vrf-sdk';

const [index] = mapRandomness(word, builtins.chooseOne(entrants.length));
const winner = entrants[Number(index)];
```

Here `word` is the giveaway's stored `bytes32` value and `entrants` is the original ordered address array. Confirm its ABI-encoded hash matches `entrantsHash`. This calculation verifies the mapping only. For a live draw, verify its public beacon and VRF evidence separately with the SDK's Robinhood replay helpers, and establish canonical chain data and the implementation active at the relevant receipts.

## Measured checks

The [included validation report](./validation-report.json) records a local run using the published SDK 0.6.2, whose protocol provenance names source commit `b733d8d144649bd92a16158fe0b5d48eb6b27b1f`. Solidity 0.8.28 compiled with no diagnostics: 5,402 bytes of creation bytecode and 4,420 bytes of runtime bytecode. An authenticated result was stored within the 100,000-gas callback allowance.

Checks cover invalid lists, underpayment rollback, request/list/mapping binding, callback authentication, request mismatch, reentrant and second-draw rejection, sponsor-only excess credit withdrawal, duplicate delivery, replacement-word rejection, 64 entrants, a zero random word, failed delivery followed by the same-word retry, expiry, failed refund notification followed by notification-only retry, and failed native refunds recoverable as backed credit.

These tests inject arbitrary words through a mock. They establish consumer behavior, not live beacon/VRF proof validity, mainnet behavior or an external security audit. No live giveaway was deployed or funded for this example.
