Two Tuesdays ago this blog went through the Chainlink VRF coordinator on Base function by function and found that thirteen of its twenty-five state-changing functions answer to a four-of-nine Safe. That is the audited layer. It has a named operator, a published signer set, and a measurable history.
Underneath it sit 1,174 live subscriptions, and every one of them has an owner of its own. Nobody audits those. I counted them tonight: 727 distinct addresses, and 617 of them are a single private key.
TL;DR
- At Base block 52,693,077 the VRF coordinator holds 1,174 active subscriptions under 727 distinct owners. 466 owners are plain keys with no code, 151 are keys running borrowed code via EIP-7702, and 110 are contracts.
- Weighted by draws that actually happened, the picture is starker: across 23,059 requests over twelve days, 95.44% ran on a subscription whose owner is one signature. Exactly 748 draws, 3.24%, sat under a multi-party owner.
- That owner cannot touch a result, but they can evict the game contract and empty the subscription. I simulated all seven calls live:
removeConsumerandcancelSubscriptionsucceed for the owner and revertMustBeSubOwnerfor everyone else. - The owner’s cancel takes the destination from calldata, not from storage. That is the opposite of the coordinator operator’s eviction function, and it happened once inside the measured window: one subscription with 301 draws behind it ceased to exist in a single block and 1.349199 ETH left with it.
- 151 subscription owners are EOAs executing somebody else’s contract. 106 of them point at the same MetaMask delegator. The contracts name themselves when you ask, and the accounts answer as the delegate, which is how I know the designators are live rather than decorative.
The layer below the audited one
A provably fair draw on Base has three parties, not two. There is the game contract, which asks. There is the coordinator, which answers with a verifiable proof. And there is a subscription, which pays, and which the coordinator checks before it lets the game ask anything at all.
Almost all public scrutiny lands on the first two. The proof is the famous part, and the funding has been looked at here before, in the sense of whether there is enough money in the account. What has not been looked at is who holds the account. So I pulled the whole population.
Everything below comes from a public Base RPC at one pinned block, 52,693,077, mined at 18:31:41 UTC on 16 October 2026. getActiveSubscriptionIds returns 1,174 identifiers. getSubscription on each of them returns the LINK balance, the native balance, a lifetime request count, the owner, and the list of consumer contracts allowed to draw against it. That is 1,174 calls, and then one eth_getCode and one eth_getTransactionCount against each of the 727 distinct owners.
What the owners are
An address on Base is one of three things, and you can tell which in a single call. Empty code means a key and nothing else. Code beginning 0xef0100 followed by twenty bytes means a key that has signed an EIP-7702 authorisation and now executes a contract stored somewhere else. Anything longer is a contract in the ordinary sense.
| Owner type | Owners | Share | Subscriptions | LINK held | ETH held |
|---|---|---|---|---|---|
| Plain key, no code | 466 | 64.1% | 696 | 942.6350 | 5.987330 |
| Key with EIP-7702 delegation | 151 | 20.8% | 294 | 237.9655 | 1.604109 |
| Contract | 110 | 15.1% | 184 | 296.6102 | 2.695142 |
| Total | 727 | 100% | 1,174 | 1,477.2107 | 10.286581 |
560 of the 727 own exactly one subscription. The largest holder owns 70. Not one of the 466 plain keys has a transaction count of zero, so none of them is a freshly generated address that has merely been named as an owner. They are all in use.
Taken at face value that table says 79.9% of the LINK and 73.8% of the native balance sitting in Base’s randomness budget is behind a single signature. But a raw census of subscriptions is a weak measurement, because the population is mostly asleep. 827 of the 1,174 have never requested randomness. 483 have no consumer contract attached at all. An empty subscription with a careless owner costs nobody anything.
Weighted by draws that actually happened
So I weighted it. I have a cached pull of every coordinator log between blocks 52,130,602 and 52,649,576, which is 18:02:31 UTC on 3 October to 18:21:39 UTC on 15 October, twelve days and four hours. In that window the coordinator took 23,059 randomness requests and emitted 23,061 fulfilments. Those requests came from 84 consumer contracts drawing against 40 subscriptions.
Forty subscriptions. Out of 1,174. That is the real population of Base’s randomness, and joining it to the owner census gives a sharper answer than the census did.
| Owner of the drawing subscription | Subscriptions | Draws | Share of all draws |
|---|---|---|---|
| Plain key, no code | 30 | 17,789 | 77.15% |
| Key with EIP-7702 delegation | 6 | 4,219 | 18.30% |
| Contract | 2 | 748 | 3.24% |
| Subscription no longer exists | 2 | 303 | 1.31% |
95.44% of the randomness consumed on Base in those twelve days ran on a subscription that one signature can switch off. The entire multi-party tail is two subscriptions. One is a four-of-nine Safe on the SafeL2 1.3.0 singleton with 655 draws behind it, which is a pleasing coincidence of threshold with the coordinator’s own operator and a completely different address. The other is a 61-byte ERC-1967 proxy that reports a threshold of one, which is governance in shape only.
One subscription alone accounts for 15,242 of the 23,059 draws, 66.1% of the chain’s randomness for the fortnight, and its owner is a plain key with no code, a transaction count of 295,734, and 0.094335479 ETH left in the subscription. That operator is clearly competent and clearly busy. That is not the point. The point is that the arrangement has no second signature in it anywhere.
What the switch actually does
Owner-gated is a claim, and a claim from reading source is a guess. So I ran all seven calls against live state at the pinned block, from the real owner address and from a stranger, and read what came back. The coordinator’s custom errors were brute-forced out of its own verified ABI rather than guessed, which is how a bare four-byte selector becomes a name.
| Call | From the owner | From a stranger |
|---|---|---|
removeConsumer on the busiest game |
succeeds | MustBeSubOwner |
addConsumer |
succeeds | not applicable |
cancelSubscription, funds to self |
succeeds | not applicable |
cancelSubscription, funds to a stranger |
succeeds | not applicable |
requestSubscriptionOwnerTransfer |
succeeds | MustBeSubOwner |
removeConsumer is the one that matters for a player. Strip the game contract off the subscription and the next request it makes is refused at the door. The game does not break; it simply stops being able to ask. Everything already delivered stays delivered, because nothing here reaches backwards into a proof. That limit is real and it held in the coordinator audit too.
The fourth row is the one I did not expect. cancelSubscription(subId, to) reads the destination from calldata. The owner names where the balance goes, and it does not have to be them. Contrast that with the coordinator operator’s eviction function, which reads the refund address out of storage and sends a cancelled subscription’s balance back to its own owner no matter who pulled the trigger. The operator, the party with the audit and the signer set, is structurally unable to misdirect the money. The individual key at the bottom of the stack has no such constraint on it.
It was pulled once, inside the window
This is not hypothetical. One of the 40 drawing subscriptions is gone, and I can date it to the block.
Subscription 50190266790512654233058731381780358102728368532297330552818831073324864881719 made 301 requests between blocks 52,402,797 and 52,432,416. At block 52,514,397 it held 1.349199164388624680 ETH, no LINK, and a lifetime request count of 301, owned by a plain key with no code. At block 52,514,398, one block later, getSubscription on that identifier reverts. The subscription does not exist. The cancellation log records 1.349199 ETH leaving to the owner’s own address, and the game has not requested randomness since.
I want to be careful about what that is. It is almost certainly an operator winding down a finished product tidily: the destination was their own address, the draws had already stopped 82,000 blocks earlier, and nobody was mid-raffle. It is housekeeping, not a heist. But it is the mechanism, exercised, on a live subscription with three hundred real draws behind it, and it took one transaction from one key.
How often the switch gets touched
The other thing the log cache will tell you is how routine all of this is. Across those twelve days the coordinator recorded 38 consumer additions, 26 consumer removals, 15 subscriptions created, 3 cancelled, and 2 completed ownership transfers, each of the latter preceded by its matching request.
The 26 removals are the detail worth sitting with, because all 26 belong to one subscription, and they are spaced almost exactly 21,600 blocks apart. That is twelve hours, to the block, over and over. Whoever runs that game rotates its consumer contract on a schedule: deploy the new one, add it, remove the old one, twice a day, forever. They made 26 draws in the same period, so the rotation is the point rather than a side effect of volume.
I mention it because it reframes what removeConsumer is. It is not a break-glass function that sits untouched for years and only ever fires in an incident. On this chain it is a twice-daily deployment step that one operator has run 26 times in a fortnight without anything going wrong. The same call, from the same owner, with the same authorisation check, is also the one that strands a live game. Nothing in the coordinator distinguishes the two, and nothing in the log would let a player tell them apart until the draws stopped.
Neither of the two ownership transfers touched a subscription that drew in the window, so the keys behind Base’s live randomness did not change hands this fortnight. Over twelve days that is a reassuring number. It is also two data points.
The 151 keys that are not quite keys
Then there is the middle row of the census, which I did not go looking for and which turns out to be the most interesting thing in the dataset. 151 of the 727 owners, 20.8%, are EOAs carrying an EIP-7702 delegation designator. They are keys in the sense that a signature controls them, and programmes in the sense that the rules governing that signature live in a contract at a third address.
Twenty distinct delegation targets, every one of which has code. The heavy concentration is striking:
| Delegation target | Owners | Identified by |
|---|---|---|
EIP7702StatelessDeleGator v1 |
106 | self-declared, EIP-5267 |
EIP7702Proxy |
14 | Sourcify verified source |
CaliburEntry v1.0.0, two addresses |
11 | self-declared, EIP-5267 |
| Unverified, 7,157 bytes | 3 | nothing |
Forwarder, from Forwarder_Protected.sol |
2 | Sourcify verified source |
| Fourteen further targets, one owner each | 15 | mixed |
The long tail reads like a roll call of everything shipping account abstraction this year: AmbireAccount7702, SemiModularAccount7702, TokenPocketSimple7702Account, Simple7702Account, a SmartWallet declaring version 1.1.0, and one contract whose source file is called Biz.sol. Seven of the twenty targets are verified nowhere I could reach.
Two things make this worth a paragraph rather than a footnote. The first is that 106 owners, 70% of the delegated set, point at one contract. The second is that an authorisation is re-pointable: the key that signed it can sign another one tomorrow and the same account will execute different rules, with no notice to any game drawing against its subscription.
I did not take the designator bytes on trust. Calling eip712Domain() directly on the owner EOAs, six of the eight I probed answered as the delegate, naming themselves EIP7702StatelessDeleGator v1 and Calibur v1.0.0 from the account’s own address. Four plain-key owners given the identical call returned empty. The two delegated accounts that did not answer simply do not implement EIP-5267, which is a gap in the standard’s adoption and not in the delegation. The code is live and it executes.
None of this is a vulnerability. Delegation is mostly a wallet upgrade, and a well-built smart account with a session policy is plausibly safer than the bare key it replaced. This blog has already measured delegated accounts on Base from the other end, when it asked what it costs to pay a winner who is a programme. The finding there was about gas. The finding here is about authority, and it is simply that one fifth of the addresses holding the off-switch for Base’s randomness are governed by code that nobody drawing against them has read.
Why a fairness claim has to include this
“Provably fair” is a precise claim and it is true. A VRF proof is verified on chain before a word reaches a game, and no owner anywhere in this stack can re-roll, edit or replay a delivered result. Even a refused delivery leaves a permanent record. The proof is the part of the system that does not depend on anyone behaving.
But a fairness claim that stops at the proof is answering a question nobody is actually worried about. Players do not lie awake wondering whether the modulo was biased. They wonder whether the thing will still be running when they try to collect, and whether the money is still there. Those are liveness and custody questions, and the honest answer on Base today is that for 95.44% of draws they resolve to one key, and for 18.30% to one key running borrowed code.
Where Satoshie stands
We run raffles and coinflips on exactly this infrastructure, which means every number in this post is a description of us as much as of anyone else. The useful response is not to claim we are the exception. It is to say what the arrangement is, because the measurement above is only possible when a project has not hidden the subscription behind an unverified contract.
Three commitments, each of them checkable with the calls in the next section rather than on our word. Our subscription identifier and its owner are published, so anyone can run the census above on us specifically. Any change of subscription owner is announced before the acceptSubscriptionOwnerTransfer lands, and the two-step handover in the coordinator gives us a window in which to do it. And the consumer set is published, so a removeConsumer against one of our games is visible rather than something players discover when a draw stops answering.
That is governance by disclosure, not by multisig, and it is weaker than a multisig. The population above says it is also better than what 617 of 727 owners currently offer. Come and check the numbers against us.
Honest limits
A single key is not a scandal. Most of these subscriptions belong to small teams or solo builders, and a four-of-nine Safe is an absurd overhead for a project drawing twelve times a week. The finding is that the risk is concentrated and undisclosed, not that anybody has behaved badly.
Classification is a snapshot, not a property. A delegation can be added or removed in one transaction, so the 151 figure is true at block 52,693,077 and may be different tomorrow. The same goes for every balance quoted.
The window and the census are 44,000 blocks apart, because the log cache ends at 52,649,576 and the state is read at 52,693,077. Two of the 40 drawing subscriptions had already been cancelled by the time I read the state, which is why they appear as their own row rather than being quietly dropped.
I can count keys. I cannot count people, a limit this blog ran into when counting the coordinator’s nine signers, and it cuts the other way here: several of those 466 plain keys may well be one signer on a hardware wallet in a team with excellent off-chain process, and several may be a hot key on a VPS. Nothing on chain distinguishes them.
And the 110 contract owners are not automatically safer. Exactly one of the two that drew is a genuine multi-party Safe. The other reports a threshold of one. Contract code is not governance, it is just a different place to keep a single point of failure.
Check it yourself
None of this needs an API key. Call getActiveSubscriptionIds(0,0) on the coordinator at 0xd5d517abe5cf79b7e95ec98db0f0277788aff634 to get all 1,174 identifiers, then getSubscription on each for the owner and the consumer list. Run eth_getCode against every distinct owner and sort on the first four bytes: empty is a key, 0xef0100 is a delegated key, anything else is a contract. For the delegated ones, take bytes 4 to 23 as the target address and call eip712Domain() on the owner itself to make the account tell you what it is running.
For the weighting, pull the coordinator’s RandomWordsRequested logs over any range you like and read the subscription identifier out of the second topic. Batch carefully: three of the endpoints I used return rate-limit text that parses as a revert, and an early run of this scan reported a clean empty result that was nothing of the sort.
Then point it at us. The whole argument of provable fairness is that you should not have to take our word for any of it, and that has to include the part where we tell you who holds the switch.
📷 Photo by Matt Walsh on Unsplash


