Buy a pack in ETH. One tokenized equity drops out - NVDA, SPY, TSLA, AAPL, MSTR or COIN. Same commit-reveal, same bankroll, same ten minute refund window as the memecoin packs.
What is different is the fill, and this repository is the layer that handles it.
Stock packs exist because Voxelithic does. We built and integrated them together: they route the fills, we run the packs. Their book aggregates six venues on Robinhood Chain behind one interface - best price or nothing, as their bio puts it - and a pack asking for one share at a time is exactly the size that needs it.
Three problems came out of that collaboration, and each one shaped the code.
This is the one that would have quietly ruined the product.
On Robinhood Chain, thirty-nine contracts answer to a stock symbol that is not theirs. The deepest of them holds over half a million dollars of liquidity while trading pennies a day. A pack that resolved tickers by searching an indexer would eventually drop one of those instead of the share the buyer paid for, and nothing on the explorer would look wrong.
So nothing here searches. Every address comes from
voxelithic-interfaces, which is
generated from the router's own configuration and checked against the chain at
build time: symbol() and decimals() have to match, and an address with no
contract fails their build. The address this package resolves to is the address
the router actually executes against, which is the only useful definition of
"the right one" when a swap is about to happen.
import { resolve, symbolOf, checkLineUp } from "@shellr/stock-packs";
resolve("NVDA"); // 0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec
resolve("NOTREAL") // throws. Never falls back to a lookup.
// And the reverse, because the contract stores addresses and nothing else.
symbolOf("0xd0601ce157db5bdc3162bbac2a2c8af5320d9eec"); // "NVDA"npm run lineup reads stocks(i) off the contract and diffs it against
LISTED. The keeper runs it on start; CI runs it nightly. A drift means
somebody called addStock without opening a PR here, and it is far cheaper to
find out on a Tuesday than during a reveal.
The full picture of what else carries these tickers is at voxelithic.xyz/registry, machine readable at /tokens.json. Worth reading once, so the paragraph above stops sounding like paranoia.
Voxelithic's quoter reverts with its answer. That is a perfectly good design
from a staticcall and useless from inside a transaction that has to carry on
afterwards.
So ShellrStockPacks.reveal takes hops and minOut as arguments. The
route is fetched up here and rides into the reveal:
import { quoteStock, minOutFor, RETRY_LADDER } from "@shellr/stock-packs";
const quote = await quoteStock("NVDA", spendWei);
const minOut = minOutFor(quote, RETRY_LADDER[0]); // 150 bps
// -> reveal(packId, secret, minOut, quote.deadline, quote.hops)That looks like the contract trusting the keeper. It is not - see below.
On pools this thin, a quote taken a second before the reveal will be stale by
the time it lands, often. A reveal that reverted on VoxSlippage used to mean
the buyer waited out the whole refund window for nothing.
So the keeper is allowed to come back with a wider minOut. What it is not
allowed to do is come back with any minOut:
require(floorPerEth[token] > 0, "no floor set");
require(minOut >= (spend * floorPerEth[token]) / 1e18, "minOut below floor");floorPerEth is set by the owner, not the keeper. So the worst a leaked
keeper key can do is execute at the floor - it cannot set minOut to one wei
and hand the pack to a sandwich it controls. Zero means the stock cannot be
revealed at all, because failing closed is the right default for the one number
standing between a stolen key and the bankroll.
The retry ladder is [150, 300, 600, 1200] basis points. It can be as wrong as
it likes and the buyer still cannot be handed dust.
WETH, not native ETH. Voxelithic's router is nonpayable and its v4 twin
carries a NativeNotSupported error. The stake is wrapped and approved inside
the contract before the swap, which the Uniswap path never had to do.
One share, not nine coins. A memecoin pack spreads its stake over slices so
one thin pool cannot spoil the draw. A stock pack puts the whole stake through
a single swap - simpler and cheaper, and it concentrates the slippage. That
concentration is precisely what floorPerEth bounds.
Seven bands, fixed at deployment. A house that can retune the odds after people have bought is not running published odds, it is running a dial.
They are public state on ShellrStockPacks - bandChance, bandLo, bandHi -
and this package reads them rather than restating them:
import { readBands, expectedPayoutBps, aboveEvenShare, topPayoutBps } from "@shellr/stock-packs";
const bands = await readBands(client, stockPacksAddress);
expectedPayoutBps(bands); // the mean multiplier, in bps of the stake
aboveEvenShare(bands); // the share of packs that land at or above cost
topPayoutBps(bands.hi); // the bankroll's exposureNothing is hardcoded here on purpose. A band table written into a README is a table that keeps getting quoted after a redeploy moves it, and the number people actually care about is the one the contract will enforce on their pack.
The stock and memecoin packs are tuned to the same mean, to within a rounding error. Two products whose published odds disagree invite the question of which one is the real house edge, and the honest answer is that they are the same business - but check both on chain rather than believing that sentence.
import { readFill } from "@shellr/stock-packs";
const receipt = await client.getTransactionReceipt({ hash });
const fill = readFill(receipt, stockPacksAddress, quote);
// { symbol: "NVDA", amountIn, amountOut, slippageBps: -87 }The quote said one number and the chain did another; the gap is the whole
question when somebody complains. PackOpened carries the token, the spend and
what was received, so a receipt answers it without an indexer and without
trusting anything in this repository.
Voxelithic publish the same idea from their side at Voxelithicag/verify. When the argument is about the route rather than the pack, run both against the same receipt.
The stock keeper derives its master secret from the memecoin one rather than sharing it:
const MASTER = keccak256(encodePacked(["bytes32", "string"], [envMaster, "stocks"]));Secrets are keccak256(master ++ index) and both contracts number their
queues from zero. Sharing a master would make stock pack #5 and meme pack #5
share a secret - and ShellrPacks.PackOpened publishes its secret on reveal.
Anyone could have read a settled meme pack, learned the secret the next stock
pack would use, ground clientSeed offline until the draw came out at 1.40x on
the ticker they wanted, and bought with it. The bankroll would have emptied in a
handful of packs and every one of them would have looked legitimate on chain.
Domain separation fixes it without a second secret to keep safe. If you are integrating two commit-reveal contracts against one master, this is the failure to look for first.
npm install
npm test # 24 tests, no RPC needed
npm run lineup # diff the on-chain line-up against LISTEDlineup needs an RPC and STOCK_PACKS_ADDRESS in the environment. No contract
address is committed to this repository, so every entry point takes one.
| shellr-contracts | ShellrStockPacks.sol and the memecoin packs |
| shellr-keeper | src/stocks.js - the stock-pack keeper |
| shellr-sdk | Reads and verifyPack for the memecoin side |
| shellr-web | /stocks - the desk |
| Voxelithicag/interfaces | Addresses, ABIs, types. The dependency this repository is built on |
| Voxelithicag/api | The quote endpoint used above |
| Voxelithicag/docs | Their architecture and integration guide |
MIT. See LICENSE.