Stock tokens on Base: field notes
Coinbase tokenized stocks live on Base as B20 tokens. We have been building a covered call vault on TSLAc for Base Stocks Maxxing. Most of what slowed us down was not in the vault. It was in four places where these tokens behave differently from the ERC-20s and price feeds the usual tooling expects. These notes are what we measured, so the next builder can plan for it instead of finding it.
1. The token is a precompile, so a stock fork cannot run it
A B20 token is implemented inside the Base node, not deployed as contract bytecode. Ask for the code at the TSLAc address, 0xb2000000000000000000001e800a7f5189430cD0, and eth_getCode returns a single byte: 0xef. That byte is a marker, not a program.
On a local fork made with stock Foundry, the marker is all there is. A call to the token on anvil 1.5.1 fails with OpcodeNotFound. Inside forge test the same call simply burns all the gas it is given, which looks like an out of gas bug in your own contract until you know. Base ships its own Foundry build, base-anvil, which hosts the precompiles locally, and base-std with the interfaces and reference mocks. Use those when you need the full token.
What worked for us, with stock tools and a public RPC, was an eth_call with a state override. The override swaps the code at chosen addresses for the length of one call. We put a small executor at a real TSLAc holder and at a few unused addresses, put an orchestrator at one more, and sent one read only call. The node ran the real precompile; nothing was signed and the chain did not change. mainnet.base.org, base-rpc.publicnode.com and base.drpc.org all accepted the override and returned identical bytes.
2. What the real token does, and what an approval does not tell you
At block 57048991 we ran a 41 step probe against live TSLAc this way, then wrote a one file mock that has to return the same bytes at every step. A few results worth knowing before you write error handling:
- Errors carry arguments in the OpenZeppelin style:
InsufficientBalance(sender, balance, needed)andInsufficientAllowance(spender, allowance, needed). transferFromchecks the receiver first, then the sender, then the allowance, then the balance. A call that fails two ways reports the first one in that order.InvalidReceiverfires for the zero address and for the token's own address, and for nothing else.- An allowance of
type(uint256).maxis never spent down. - An unknown function selector reverts with that selector as the revert data.
- In separate reads on 12 November, TSLAc reported policy id 5 on all three transfer scopes (sender, receiver, executor), and the vault, our wallet and a random address all read as authorized, so in our reads it behaves as a blocklist, not an allowlist.
The point that matters most for contract design is in Base's own IB20 reference: approve is not gated by any policy or by pause. Transfers are checked against the sender and receiver policy slots, and transferFrom also checks the caller as executor when the caller is not the owner. So an approval to your vault can succeed while the deposit that spends it would fail. A successful approve tells you nothing about whether the transfer will go through. If your flow approves in one step and pulls in another, handle PolicyForbids and ContractPaused on the pull, and test both. We could not trigger those two paths live, because only the issuer can block or pause, so our mock follows the docs for them and says so.
3. The price feed was silent on 14 of 14 weekends
We read every round of the Coinbase TSLA Chainlink feed (0xFaf869185383a24F8cb00e27BdA6b63B9905DCb4) at Base block 57036280: 1,554 rounds across two aggregator phases, from 3 August to 13 November 2026. The figures here use the current phase only, 1,407 rounds over 99.8 days, so nothing is counted twice.
- Every weekend went quiet. All 14 complete weekends had no round for 49.0 to 76.0 hours. The median gap was 52.7 hours; the 76.0 was Labor Day weekend.
- It came back on a schedule. On 13 of 14 weekends the first round arrived within 2 minutes of Sunday 8 PM New York time: 00:00 UTC on Monday in summer, 01:00 UTC since the November clock change. The exception was the holiday Monday.
- The last price before the weekend was often not the close. On 9 of 14 Fridays no round landed between the 4 PM New York close and the reopen. The earliest Friday "last round" was 15:47 UTC on 11 September, more than four hours before the close.
- Rounds follow price, not the clock. 99.1% of the moves between consecutive rounds were 0.50% or larger, median 0.53%. Once, from a Wednesday evening to Thursday afternoon, the feed went 18.6 hours without a round.
- Short staleness bounds lock you out. The newest round was more than 1 hour old 72.8% of the time and more than 4 hours old 45.4% of the time. Leaving out the weekend gaps, those shares were 60.9% and 23.3%.
We ran the same scripts on the Coinbase AAPL feed (0x787f13dEa48Db0897CbCDD985de77809D837F988, block 57042108) and saw the same shape: 14 of 14 weekends silent. This is not a fault in the feed. Base's docs describe these feeds as 24/5, with the value held off hours, and the rounds match that. What the docs do not tell you is what that schedule does to contract logic, so here it is in numbers.
| Friday | Last round before (UTC) | First round after (UTC) | Gap h | Round after close? | Move |
|---|---|---|---|---|---|
| 08-07 | Fri 16:49 | Mon 00:00 | 55.2 | no | +37.5 bps |
| 08-14 | Fri 18:19 | Mon 00:00 | 53.7 | no | +17.5 bps |
| 08-21 | Fri 23:00 | Mon 00:00 | 49.0 | yes | +51.9 bps |
| 08-28 | Fri 19:54 | Mon 00:00 | 52.1 | no | −25.3 bps |
| 09-04 | Fri 20:02 | Tue 00:00 | 76.0 | yes | +23.3 bps |
| 09-11 | Fri 15:47 | Mon 00:00 | 56.2 | no | −101.2 bps |
| 09-18 | Fri 19:49 | Mon 00:00 | 52.2 | no | +23.1 bps |
| 09-25 | Fri 18:52 | Mon 00:00 | 53.1 | no | +10.0 bps |
| 10-02 | Fri 19:55 | Mon 00:00 | 52.1 | no | +52.1 bps |
| 10-09 | Fri 16:05 | Mon 00:00 | 55.9 | no | +16.5 bps |
| 10-16 | Fri 20:44 | Mon 00:00 | 51.3 | yes | +123.8 bps |
| 10-23 | Fri 20:05 | Mon 00:00 | 51.9 | yes | +39.8 bps |
| 10-30 | Fri 19:08 | Mon 01:00 | 53.9 | no | +48.7 bps |
| 11-06 | Fri 21:42 | Mon 01:00 | 51.3 | yes | −22.7 bps |
What this means in code:
- A staleness check sized for a crypto feed, say one hour, rejects this feed most of the day. A bound that keeps weekdays open (the longest weekday gap was 18.6 hours) still closes every weekend. That may be exactly what you want. Decide it on purpose and test both sides.
- "Settle on the first round at or after expiry" with an expiry at the Friday close gave the Sunday night reopen price on 9 of 14 weekends, 52 to 53 hours later and after the weekend's news. If you want Friday's price, put the expiry inside the trading day, or settle on the last round before expiry.
- Even when a round lands near the close, it is the last 0.5% step, not necessarily the closing price.
Our own vault sells options only against a round under four hours old, so it cannot open a sale over a weekend, and a Friday evening expiry may not settle until the Monday reopen.
4. A venue withheld the swap button
To run the vault's first epoch with our own funds, we needed about $20 of TSLAc. On 13 November we connected our wallet to the Aerodrome swap page and selected USDC to TSLAc. The page returned a quote, so routing and liquidity were fine. Where the swap button normally sits, it showed a notice headed "Restricted Tokenized Security" pointing to the issuer's information, and the buy field was disabled. With the same wallet, USDC to AERO showed the button as usual.
We do not know whether the notice depends on the visitor's location or applies to everyone, and we are not going to guess. We did not try any other route. We stopped, and ran the epoch on the Base Sepolia testnet instead: the same vault creation code as our mainnet deploy, a substitute token with no value, and a feed that copies the real Chainlink TSLA rounds. Both wallets in that epoch are ours. It is a test of the mechanics, not usage.
The design lesson is plain. Do not assume your users can acquire the asset your contract holds. These tokens are restricted securities, and the issuer's published terms exclude several groups of buyers. A vault, a lending market or an options product on these tokens depends on someone being allowed to buy the underlying, and that is decided outside your contract. Plan your launch, your test runs and your own seed deposit around it, and say clearly in your interface who can and cannot take part.
A short checklist
- Test against the real precompile with a state override
eth_call, or withbase-anvil. A stock fork will mislead you. - Match error handling to the real error arguments and check order, not to a generic ERC-20.
- Treat a successful
approveas no signal at all. HandlePolicyForbidsandContractPausedwhere the transfer happens. - Choose your staleness bound and your expiry times against the feed's real schedule, and replay recorded rounds in your tests.
- Find out early whether your users, and you, can lawfully get the token.
Sources and raw data
Everything above can be read again from public RPCs. The files are hosted with this page:
- tsla_feed_rounds.csv: all 1,554 rounds, with the block, block hash and RPC they were read at.
- tsla_feed_weekends.csv and tsla_feed_summary.json: the weekend table and every figure in section 3.
- tslac_live_probe.json: what live TSLAc returned to each of the 41 probe calls at block 57048991.
- METHOD.md: how the rounds were read and how gaps, staleness and step sizes were computed. It refers to the scripts in our test kit, which we are publishing separately as an MIT repository.
- Base: Tokenized stocks on Base and the IB20 reference.
- Our contracts: covered call vault on Base mainnet
0xc6180E9af7Cb4c6D47A230a6c5047fA615Bd0956(deposits closed); Base Sepolia test epoch vault0x573aD50Ec7279869Ac268B3BA8c8E6074b228CBc.
One feed and one token, measured as a snapshot. A feed's deviation threshold and schedule can change, and other B20 tokens can carry different policies and decimals. Read the data again before you rely on a number. Not affiliated with Base, Coinbase or Chainlink.