Skip to main content

You did the thing we keep telling you to do. You found the raffle contract, you read it before committing anything, and the prize pool came back as a number: 10,000 tokens, escrowed, sitting at an address you can check. Not a figure on a marketing page. Not a balance in somebody’s database. A number the chain gave you.

Good. That is genuinely more than almost any gaming platform will let you do, and the habit is the right habit. But there is a gap in it that nobody in this industry talks about, and it is not about whether the draw is rigged, whether the operator is solvent, or whether anyone is allowed to stop you collecting. Those are all real questions and we have written about all three. This is a different one, and it is smaller and stranger than any of them.

You did not read a fact. You called a function. And for an entire class of tokens, that function returns a different answer tomorrow with nobody having touched anything.

TL;DR

  • A token balance is computed, not stored. Lido’s own documentation states the formula outright: balanceOf(account) = shares[account] * totalPooledEther / totalShares, and notes that “balances are dynamic”. A contract’s stETH balance moves on every oracle report without a single transfer.
  • We checked the live numbers. At Ethereum block 26,092,119, one Lido share was worth 1.245250558 stETH. A contract showing a 10,000 stETH prize holds 8,030.51 shares, and the prize number climbs daily on its own.
  • Transferring a token does not always conserve it. Balancer lost roughly $450,000 across two pools on 29 June 2020 because STA charged a 1% fee on every transfer and, in Balancer’s own words, “the pool expects it receive a balance without the fee”.
  • The number has a unit and the unit is not universal. Verified on-chain: USDC has 6 decimals, USDT 6, WBTC 8, stETH 18. “1000” means four different quantities depending on which contract you asked.
  • This is not the value question. Whether a prize is worth what it claims is a separate problem from whether it is still the same quantity. A rebasing token can hold its peg perfectly and still hand a winner a different number than the one they read.

What “escrowed on-chain” actually promises

Start with what the claim is good for, because it is not nothing.

When a prize is escrowed in the contract rather than promised by the operator, you get something a centralised casino cannot offer at any price: the prize is not a liability, it is a position. Nobody has to stay in business for it to exist. There is no withdrawal queue, no treasury policy, no “processing” state. The stake and the payout can be made to happen in the same transaction, which means the question “will they pay” collapses into “does the code run”, and the code is readable before you commit.

That is the strongest structural claim in on-chain gaming and we make it constantly. What it promises, precisely, is custody and control. It says: the value is here, and no discretionary human step stands between the draw resolving and you holding it.

What it does not promise is that the number you read is a measurement of anything stable. “The prize is a balance you can read, not a number you believe” is a line we have used, and it is true in the sense that matters most. But a balance is a reading off an instrument, and the instrument belongs to somebody else.

balanceOf is a derivation

Most people carry a mental model of ERC-20 that goes like this: the token contract keeps a ledger, a big mapping from addresses to amounts, and balanceOf looks yours up. Deposit moves the number up, withdraw moves it down, and between transactions it sits there.

For a great many tokens that is exactly right. For an important minority it is not even close.

Lido’s stETH is the cleanest example because Lido documents it plainly rather than hiding it. stETH is a rebasing token, and the protocol’s docs describe the mechanism without euphemism: “Instead of storing a map with account balances, Lido stores which share of the total pool is owned by the account.” The balance is then calculated as shares[account] * totalPooledEther / totalShares. Under balanceOf the documentation carries a one-line note that ought to be printed on the inside of every smart contract developer’s eyelids: “Balances are dynamic and equal the account’s share in the amount of total ether controlled by the protocol.”

Dynamic. Not stored. Derived, on the fly, from two global variables that a daily oracle report moves.

We queried the live contract rather than taking anyone’s word for it. At Ethereum block 26,092,119, Lido reported roughly 9,839,538 ETH pooled against roughly 7,901,653 shares, and getPooledEthByShares confirmed the ratio directly: one share, meaning 1e18 share units, corresponded to 1,245,250,558,309,363,625 wei of stETH. That is 1.245250558 stETH per share.

Now put a prize in it. A raffle contract holding what its interface calls a 10,000 stETH prize is, in the token’s own accounting, holding 8,030.51 shares. Tomorrow’s oracle report lands, totalPooledEther rises with staking rewards, and the same 8,030.51 shares render as something above 10,000. No transaction. No admin. No bug. The number on the page went up because the number on the page was never a stored quantity in the first place.

That happens to be the pleasant direction, and it is why rebasing tokens get treated as harmless. They are not symmetrical, though. The same formula runs on slashing penalties, and Lido’s documentation says so: the supply “is increased or decreased algorithmically”. A contract that hardcodes an expected payout figure, or that checks a stored constant against a live balance, has written an assumption it never stated and cannot enforce.

Here is the detail that settles the argument. Lido ships transferShares() and transferSharesFrom() alongside the standard ERC-20 transfer functions: a second transfer path denominated in shares rather than in tokens. That exists because the issuer understood that anyone doing serious accounting in stETH needs a unit that does not move underneath them. The escape hatch is right there in the ABI. Most integrations never call it.

Transfer is not conservation

The second assumption is worse, because it fails at the exact moment a prize is paid rather than while it sits.

Almost every contract that moves tokens is written as though transfer(recipient, 100) causes the recipient’s balance to rise by 100. The ERC-20 standard does not require that, and tokens exist that charge a fee on every transfer, burning or diverting a slice in transit. Send 100, the recipient receives 99.

This is not a hypothetical. On 29 June 2020 an attacker drained two Balancer pools of roughly $450,000, and the vulnerability was precisely this. Balancer’s own incident write-up describes the mechanism in a single sentence: “On each trade, STA has a transfer fee and the pool expects it receive a balance without the fee.” The attacker took a flash loan, traded WETH against STA repeatedly, and each round trip shaved 1% off the STA the pool actually held while the pool’s internal accounting believed it held the full amount. Drive the recorded balance close to zero and STA’s price inside the pool becomes enormous, at which point you buy the rest of the pool with dust.

Read Balancer’s conclusion carefully, because it is the honest version of what every protocol in this space is quietly relying on: “The system is designed for compliant ERC20’s and when tokens behave unintended ways, bad things can happen.” They had warned about transfer-fee tokens in their documentation beforehand. They had deliberately kept STA off a mining whitelist. The pools were drained anyway, because a permissionless protocol cannot stop anyone adding a token at the contract level. Balancer reimbursed the affected liquidity providers in full, which was the right call and also an expensive one.

Note the other blacklist mentioned in that same post-mortem: “no bool transfer tokens”, meaning ERC-20s whose transfer function returns nothing at all where the standard says it returns a boolean. That is a second, entirely separate way the same assumption breaks. There is a reason defensive transfer wrappers exist in every serious contract library, and the reason is that the standard is a suggestion with a very large installed base of exceptions.

The number has a unit

The third one is the most mundane and by far the most common, and it is simply that token amounts are integers with an externally declared scaling factor.

We read the decimals() function on four major tokens directly from Ethereum mainnet. USDC returns 6. USDT returns 6. WBTC returns 8. stETH returns 18. There is no canonical value. The standard treats decimals as display metadata and explicitly permits any of this.

So “the prize is 1000” is not a quantity. It is a quantity once you also know which contract to divide by what. Every interface that renders a prize pool, every off-chain indexer, every line of code comparing one balance against another, is applying a scaling factor it got from somewhere, and if it got it from a hardcoded 1e18 then it is right for most tokens and wrong by a factor of a trillion for USDC.

Why this is a different question from the ones we have already asked

We are deliberate about not recycling arguments, so let us draw the lines.

This is not the value question. We have argued that a stablecoin-denominated prize inherits the issuer’s reserves and liabilities in full, and that a prize denominated in a promise is only as good as the promise. That is about whether your unit is worth what it says. This post assumes the unit holds its peg perfectly and asks whether you still have the same number of them. Those are independent failures. A rebasing token trading at exactly par can still deliver a different quantity than the one you read.

This is not the solvency question. A game can be provably fair and still unable to pay you, because database credit is not escrowed value. Here the value genuinely is escrowed. The escrow is real and the arithmetic over it is still ambiguous.

This is not the permission question. Issuer blocklists and freeze functions mean a third party can decide you may not collect. In everything above, nobody decides anything. There is no adversary in the stETH case at all. The number moves because a formula evaluates, and the formula is working exactly as designed and documented.

And it is not the randomness question. A VRF proof settles which address won. It is completely silent on how much that address receives, and it always was. Randomness integrity and payout arithmetic are different layers, and “provably fair” has only ever covered the first.

What Satoshie claims, narrowly

Satoshie raffles and coinflips settle in the chain’s native asset on Base, which sidesteps all three failure modes above rather than solving them. Native value has no balanceOf derivation, no transfer fee, no decimals() to misread. The stake is escrowed at entry, the ticket count is readable before you buy so your odds are arithmetic rather than assertion, Chainlink VRF’s coordinator verifies the proof on-chain before the callback can execute, and resolution and payout happen in one transaction with no admin key over a draw in flight.

The claim about the prize is therefore specific: the quantity you read is the quantity in the contract, in units that do not move, and it is the quantity that leaves. That is a property of the asset choice, not a property of our cleverness.

Honest limits

We took the easy branch, and we should say so. Using the native asset is a design decision that also costs us things. It rules out prizes denominated in whatever token a community actually wants, and it makes certain partnerships awkward. We did not solve the token-semantics problem. We arranged not to have it, and the day we list a prize denominated in any ERC-20 is the day every question in this post applies to us and we owe you the answers in public before taking a single entry.

The front end still renders numbers, and we still wrote the front end. Everything above is about the contract. A player reading a prize figure in a browser is reading our JavaScript’s opinion of the contract’s state, and that remains the least trustworthy thing we ship. Native-asset accounting removes a class of arithmetic error from the contract. It does not make our interface authoritative, and the fix for that has always been to read the chain yourself rather than to trust us harder.

Gas is a quantity we do not control either. A payout of exactly X leaves the winner holding X, but the transaction that got them there cost them something, and on a Layer 2 that cost is a function of L1 conditions nobody on our team sets. The prize arithmetic is exact. The net arithmetic involves a number Base computes.

Three questions worth asking any platform

  1. Which asset is the prize denominated in, and is its balance stored or derived? If the answer is a rebasing or interest-bearing token, ask what happens between the draw resolving and the payout landing.
  2. Does the payout path verify what actually arrived, or what was requested? A contract that checks the recipient’s balance before and after is defending against transfer fees. One that assumes the transfer amount is the received amount is Balancer in June 2020.
  3. Where did the interface get its decimals? From the token contract, or from a constant somebody typed.

None of this is exotic and none of it requires an attacker. It is the difference between a number and a measurement, and provable fairness has never covered it because provable fairness is a claim about how a winner is chosen. The prize is a separate sentence, and most platforms have never finished writing it.

See how Satoshie raffles and coinflips work, and read the contract before you read our marketing.

📷 Photo by Magic Fan on Unsplash

Valentina Ní Críonna

Author Valentina Ní Críonna

More posts by Valentina Ní Críonna