Quiver security model
This document states Quiver's trust assumptions, the threat model, and the mitigations for each attack. Read it before deploying with real value.
Status: Quiver has a thorough test suite (unit, adversarial, fuzz) but has not had an independent third-party audit. Treat the assumptions below as load-bearing.
1. Actors and trust
| Actor | Trusted for | Can it bias randomness? |
|---|---|---|
| Coordinator (contract) | Correct, immutable execution | No — it only combines committed values |
| Provider | Keeping seed secret until each reveal; liveness | No (bias). Yes: withhold reveals (liveness) |
| Requester | Its own contribution; liveness (pull flow) | No (bias). Yes: withhold its reveal (pull) |
| Coordinator owner | Setting protocol fee, pausing new requests | No — cannot touch in-flight randomness or seeds |
Core claim: as long as at least one of {provider, requester} is honest, the output is unbiased and unpredictable. The two contributions are each committed before either is revealed, and the result is a fixed function of both.
2. Cryptographic assumptions
keccak256is pre-image resistant — nobody can invert the published commitmentchain[N]to learn earlier values, and nobody can find a value hashing to a required anchor other than the true pre-image.keccak256is collision resistant — a provider cannot craft two distinct chains sharing a tip.- The combined output
keccak256(userRandom ‖ providerRevelation ‖ blockHash)behaves as a random oracle over inputs neither party solely controls.
3. Threats and mitigations
3.1 Provider forging or altering a revealed value
Attempt: submit a providerRevelation other than the committed chain[N-k].
Mitigation: the coordinator recomputes keccak256^numHashes(providerRevelation) and
requires it to equal the request's stored anchor. Any other value fails. The chain is
fixed at registration; the provider has no freedom. ✔ Enforced in _verifyAndResolve.
3.2 Requester submitting a mismatched revelation
Attempt: reveal a userRandom different from the committed one to steer the result.
Mitigation: keccak256(userRandom) must equal the stored userCommitment. ✔
3.3 Replay / double-reveal
Attempt: reveal the same request twice, or reuse a revelation.
Mitigation: the request slot is deleted on reveal; a second reveal hits
NoSuchRequest. Sequence numbers are monotonic and never reused. ✔
3.4 Provider selective withholding (the main liveness risk)
Setup: in the push flow, useBlockhash is off and the raw userRandom is public,
so the provider can compute the outcome as soon as the request appears — and could
decline to reveal outcomes it dislikes.
It cannot bias (the value is fixed), but it can stall.
Mitigations:
- Economic/reputation: run providers you trust or that are staked/slashable; a provider that withholds is publicly observable (open request, no reveal).
- Pull flow +
useBlockhash: fold inblockhash(requestBlock), unknown at request time, so the provider cannot even predict the outcome when deciding to reveal. - Multiple providers: consumers can choose among providers; a withholding provider loses fees and reputation.
- Timeouts (app-level): design your consumer to allow re-requesting from another provider if a request isn't fulfilled within a deadline.
3.5 Requester withholding (pull flow)
Attempt: a requester that dislikes the (secret-to-others) outcome never reveals. Impact: only its own request stalls; it already paid the fee. No effect on others. Note: the requester cannot know the outcome before revealing (it doesn't have the provider's value), so this is not even a useful attack — just abandonment.
3.6 Block-producer / sequencer manipulation
With useBlockhash: on an Arbitrum Orbit chain the sequencer produces blocks and
influences blockhash. Folding blockhash adds entropy the provider can't predict,
but a malicious sequencer colluding with a party could grind block hashes.
Guidance: blockhash is a hardening input, not the root of trust — the two-party
commitment is. For adversarial-sequencer threat models, rely on the provider+user
commitments (both fixed) and treat blockhash as optional defense-in-depth. Never make
blockhash the sole entropy source.
3.7 Callback griefing
Attempt: a consumer whose quiverCallback always reverts or burns gas, to block the
provider's reveal tx / waste its gas.
Mitigation: revealWithCallback completes all state changes first, then calls the
consumer inside try/catch. A revert/out-of-gas is caught; the randomness is buffered
(CallbackFailed) and the provider's tx still succeeds and earns the fee. The consumer
recovers via retryCallback. The griefer harms only itself. Fletcher additionally
simulates the reveal before sending. ✔
3.8 Reentrancy
Surface: the callback and native-token transfers (fee refunds, withdrawals).
Mitigations: reveals, withdrawals, and retryCallback are nonReentrant; all follow
checks-effects-interactions (request deleted / balances updated before external calls).
request refunds overpayment after writing state and advancing the sequence number,
so a reentrant request from the refund cannot reuse a sequence number. ✔
3.9 Gas-griefing via long hash chains
Attempt: force a reveal to compute an enormous number of hashes.
Mitigation: numHashes is bounded at request time by the provider's maxNumHashes
(itself capped by MAX_NUM_HASHES = 10_000). Prompt reveals keep it at 1. ✔
3.10 Seed compromise
Impact: anyone who learns a provider's seed can predict all its future values.
Mitigations: treat the seed like a private key (see
fletcher-operations.md); on suspicion, rotate immediately
(rotateCommitment) — all future values use a fresh seed and are safe again. In-flight
requests on the old chain remain as-committed.
3.11 Admin powers
The coordinator owner (Ownable2Step) can:
- set the protocol fee (affects only future request pricing),
- withdraw accrued protocol fees,
pause()new requests (in-flight reveals and retries stay enabled).
The owner cannot alter seeds, forge values, change in-flight requests, or seize provider fees. Use a multisig/timelock for the owner in production, and consider renouncing pause once stable. Two-step ownership transfer prevents fat-fingering the owner to an inaccessible address.
4. Randomness quality notes
- Uniformity & range reduction. The raw output is a uniform 256-bit value. Reduce to
a range with modulo — the modulo bias for a small range over 2²⁵⁶ is ~
range / 2²⁵⁶, i.e. cryptographically negligible. For a d6, bias ≈ 1/2²⁵³. - Independence. Each request consumes a distinct provider value and a distinct user value, so
outputs are independent. Never reuse a
userRandomNumberacross requests. - Do not derive multiple "independent" draws by re-hashing one output with attacker- known inputs. If you need N independent values, request N times or expand with a domain-separated KDF over the single verifiable output.
5. Deployment hardening checklist
- Owner is a multisig (and ideally behind a timelock).
- Providers you rely on are ones you operate or that are economically accountable.
- Consumers treat fulfillment as async and tolerate/retry withholding.
- Consumer callbacks are lean, idempotent, and never assume they can't be retried.
- Seeds are generated with a CSPRNG, stored like private keys, and rotation is rehearsed.
- For high-value use, prefer the pull flow with
useBlockhash, or add provider staking. - Independent audit before mainnet value at scale.
6. Responsible disclosure
Found a vulnerability? Do not open a public issue. Contact the maintainers privately with a description and, ideally, a proof-of-concept. (Wire up a security contact / bug-bounty before mainnet.)