Technical details
This section documents the protocol implementation for readers who want to inspect Rafflor below the product layer.
It covers:
- randomness provider and round selection;
- onchain signature verification;
- settlement incentives;
- refund-mode mechanics;
- escrow isolation;
- unusual ERC-20 behaviour;
- ERC-721 delivery behaviour;
- contract invariants;
- live-chain assumptions and tests;
- source and deployed-bytecode verification.
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:
- The round is not chosen at creation. A round picked when the raffle opened would be a round whose value could become public while tickets were still selling.
- The margin exists because "at close" is not a precise instant. Block time drifts. Without it, somebody buying the last ticket as the beacon published would be entering a raffle whose seed already existed. It is fixed when the resolver is deployed and cannot be changed afterwards.
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:
- every ticket is refunded at face value, pull-based;
- the creator takes the prize back;
- the creator receives nothing. No revenue, no share of anything.
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 token's contract defines the asset. For a USDC prize that is this chain's USDC predeploy — the same contract behind every USDC balance on the chain. It holds nobody's prize; it records everybody's balance.
- The vault at 0x6485327b086B492268AE86F2A1b5304fE793e206 holds the ticket money and runs the raffle. It never holds a prize.
- The raffle's escrow holds the prize, and holds nothing else at all.
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.
- A token that takes a cut on transfer cannot make the escrow lie. What is recorded is the balance that actually arrived, measured across the transfer, so the raffle advertises the smaller figure from the start.
- A token that reverts, is paused, or has blacklisted a party costs its own prize. It cannot touch the ticket USDC, because claiming the prize and claiming the revenue are separate functions that touch different contracts.
- A token whose supply moves after the escrow — a rebase, an owner burn — delivers what is actually there. The claim pays the smaller of the recorded amount and the escrow's balance at the moment of the claim. Demonstrated on chain: a 1,000-token prize whose escrow was burned down to 400 paid 400. This is why a prize claim is never offered as an amount: the figure on a raffle is what arrived, not what will leave.
- Surplus is stranded on purpose. The recorded figure is a ceiling too, so a token whose supply moves up does not enlarge the prize, and nothing can reach the difference afterwards.
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
- A winner cannot be changed. The winner is derived from the seed and the ticket ranges, in the transaction that settles. There is no stored winner to overwrite and no function that sets one.
- A seed cannot be changed. It is the hash of a drand signature the resolver verified.
- An escrow cannot be moved. Each raffle's prize is in its own address, and the only caller that address accepts is the vault.
- Ticket money cannot be taken. It is owed to the buyers until a draw makes it the creator's, and owed back to them if the draw never happens.
- A raffle is immutable from creation and locked from its first entry. Before any ticket sells the creator can take the prize back and cancel; after one, nothing about the raffle changes.
- Changing a protocol parameter means deploying a new vault. Raffles on this one stay on it, under the terms they were created with, for as long as they run. That is why raffles from more than one vault can appear on the same board and why each one says which vault it is on.
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.
- The vault: [0x6485327b086B492268AE86F2A1b5304fE793e206]([unavailable: no block explorer is configured for this chain]/address/0x6485327b086B492268AE86F2A1b5304fE793e206)
- The resolver: [0x3eCcdC848f863A21f702003fBb90Fa7d72cB2961]([unavailable: no block explorer is configured for this chain]/address/0x3eCcdC848f863A21f702003fBb90Fa7d72cB2961)
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:
- Take the round number from the raffle page.
- Fetch that round from drand:
https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/<round>. - The
signaturethere is the one the contract verified. The seed is itskeccak256. - 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.
[unavailable] above rather than as a number from memory:
- treasuryAddress — TREASURY_ADDRESS is not configured
- explorer — no block explorer is configured for this chain