Rafflor

Technical details

This section documents the protocol implementation for readers who want to inspect Rafflor below the product layer.

It covers:

These details are not required to use Rafflor.

Randomness provider and round selection

drand is a public randomness beacon. A group of independent operators jointly signs a counter — round 1, round 2, and so on — every 3 seconds, and publishes the signature. No single operator can produce a valid signature alone, and the signature for a round is determined by the round number: there is exactly one, and it does not exist until the network produces it.

The resolver at 0x3eCcdC848f863A21f702003fBb90Fa7d72cB2961 verifies against the drand chain called quicknet, on BLS12-381:

chain hash   52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
period       3 seconds

You can read the beacon yourself at https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest — it is drand's API and nothing of ours sits in front of it.

The target round is fixed when the raffle closes. When a raffle closes — because it sold out, or because its deadline passed — the contract computes a target round from the close time plus a margin, and records it. That happens before the round has been published.

round          = roundAt(closeTimestamp + margin)
closeTimestamp = the sold-out transaction's block time, or the deadline
margin         = 1 minute

Two things follow, and they are the whole design:

No entry lands after the close, by any path. That is the invariant the rest of this rests on: if an entry could arrive after the round is determined, it would not be a raffle.

Onchain signature verification

Once the round publishes, anyone may hand the signature to the resolver. There is no privileged submitter and no queue. The contract verifies the signature against drand's group public key using the BLS pairing precompiles on this chain, and only a valid signature for the recorded round is accepted.

The resolver publishes that public key itself, as publicKey() — a G2 point, uncompressed. It is a constant of the beacon rather than a parameter of ours, so it is not restated here: read it from the contract and compare it with drand's own published key, masking the three compression flag bits off the first byte. A wrong y-coordinate cannot be checked by eye and does not need to be, because a wrong y fails the pairing and the resolver verifies a known round in its own constructor.

The seed is keccak256 of that signature. The winner is found by binary search over cumulative ticket ranges, so the cost of settling does not grow with the number of entrants.

What a submitter can and cannot influence

Cannot: the signature. It is determined by the round number and produced by drand, and the resolver accepts no other. There is exactly one valid signature for the target round, so there is nothing for a submitter to choose between.

Cannot: the round. It was recorded at close, before the signature existed.

Cannot: the encoding. The same signature has a short and a long form whose hashes differ, and accepting both would hand the submitter a choice between two seeds. The resolver accepts one form only.

Can: when the raffle settles, by submitting sooner or later — and whether it settles at all, by not submitting. That is why anyone can submit and why the tip exists.

One thing that is not the submitter's, and is worth naming. The close timestamp is a block timestamp, so a party who can move block timestamps could move the target round. The margin is set to exceed the chain's drift for that reason. The mover there would be a block proposer, not a submitter.

Settlement incentives

A tip of $0.05 is paid to whoever submits the beacon, out of the protocol's share rather than out of anybody's prize or ticket revenue.

Why a tip is paid at all. The moment drand publishes, the winner is computable by anybody, and a refund returns every ticket — so every losing player would prefer that nobody submits. Nobody has to collude and nobody has to act; the outcome that unwinds the raffle is the one that happens if everybody does nothing. A tip that covers the cost breaks that, because it only takes one indifferent third party for whom submitting is profitable.

Somebody has to pay for the transaction that pushes the raffle forward, and it should not be the winner.

Refund-mode mechanics

The beacon can be submitted for 7 days after the raffle closes. If no valid signature arrives in that window, the round can never be resolved and the raffle enters refund mode:

Refund mode cannot be forced by anyone, because anyone can submit the beacon. There is no party whose inaction triggers it that another party cannot simply act instead of.

The same applies to closing. Anyone can close a raffle, including any player, and it costs one transaction. If nobody does for a whole claim window past the deadline, anyone can put the raffle into refund mode instead — so a raffle cannot be left holding ticket money indefinitely by everybody doing nothing.

What this section cannot claim: a refund has not been completed on a live chain. The route is reachable only after the 7 days above, and that timer cannot be shortened — there is no admin and no warp — so it is proven by the test suite and by a raffle deliberately left waiting, and not yet by a completed refund.

Escrow isolation

Three addresses are involved in a prize and they are easy to confuse:

The escrow is a contract of its own, deployed per raffle — an EIP-1167 minimal proxy, created in the same transaction as the raffle, with its address recorded on the raffle and in the RaffleCreated log. Every raffle has a different one. escrowOf(raffleId) returns it, and a raffle's page prints it.

It is a proxy, and the no-upgrade rule still holds, because that rule is about upgradeability. A minimal proxy has the address it delegates to baked into its own runtime code: there is no admin slot, no initialiser and nothing to write. What it delegates to on its last day is what it delegated to on its first. The proxy this project refuses is the kind that can be pointed somewhere else, and no escrow can be.

Two things follow. A badly behaved prize token costs its own raffle's prize and nothing else, because there is no shared balance for it to reach into. And the only caller the escrow accepts is the vault.

The escrow address is derivable by anyone before the raffle exists. That is deliberate, and it is what two measurement defences exist for: the ERC-20 pull takes a balance delta, so a pre-funded escrow cannot inflate what a raffle records, and the ERC-721 pull checks ownership on both sides, so a creator cannot claim an NFT somebody parked there. Both were verified on the live vault.

Ticket money is the other half and never leaves the vault until it is claimed. Nothing here is payable in the native asset and none of these contracts has a receive function.

Unusual ERC-20 behaviour

A raffle's prize figure is what the creator escrowed, measured when it arrived rather than as it was requested.

Nothing here detects any of that in advance, and none of it is a judgement about a token. A rebasing token is a design, not a defect. What can be stated is what arrived and that what leaves can be less; whether a given token behaves that way is not something this product checks.

ERC-721 delivery behaviour

An NFT prize is released with safeTransferFrom from the raffle's own escrow, so the receiving address must be able to accept one: a contract that does not implement the ERC-721 receiver hook will cause the transfer to revert, and an externally owned account is unaffected.

The revenue claim is a separate function on a separate contract and is not affected by any of this.

Contract invariants

The guardian's single power is pausing the creation of new raffles. It cannot touch a winner, an escrow, a claim, a refund or the randomness — and not because it is forbidden to. There is no function that does any of those things, for any caller. A pause does not reach a live raffle. The vault can also be deployed with no guardian at all, in which case the pause reverts for every caller including the zero address.

Live-chain assumptions and tests

The contract does not check the submitted point's subgroup itself. It relies on this chain's BLS precompiles rejecting a malformed one, which they were measured doing before the resolver was deployed. That is a property of the chain rather than of our code, it is checked by a test that runs against the real precompiles, and it is the assumption the randomness design depends on most.

If that ever changed, the failure would be silent. Invalid signatures would start verifying, nothing would revert, and every raffle would still draw a winner. It is the reason that check is a test against live precompiles rather than an assumption, and the reason it is re-measured on any chain this is deployed to.

The resolver verifies a known drand round in its own constructor, so on a chain without those precompiles it cannot be deployed at all — rather than deploying and failing on the first settlement.

The vault does not depend on this site. It does not call us, does not read from us and does not know we exist; this site is a reader over public logs. If it went away the raffles would settle anyway, and what would stop working is the interface rather than the protocol.

Source and deployed-bytecode verification

The vault's whole surface is public, and everything on this site is derived from its logs — the same logs anybody can read.

Source and the test suite are in this project's repository. The deployed bytecode of each vault version is kept verifiable against the source it was built from, in its own frozen directory, because a contract that cannot be checked against its source is a contract you are taking our word for.

Checking a draw by hand

For any drawn raffle, its page shows the target round, the seed and who submitted it. To check the draw:

  1. Take the round number from the raffle page.
  2. Fetch that round from drand: https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/<round>.
  3. The signature there is the one the contract verified. The seed is its keccak256.
  4. The winning ticket is the seed modulo the number of tickets sold, and the raffle's entries say which address holds it.

Nothing in that chain of steps passes through us.

2 figures on this site could not be read from the chain, so they appear as [unavailable] above rather than as a number from memory:
  • treasuryAddress — TREASURY_ADDRESS is not configured
  • explorer — no block explorer is configured for this chain