Skip to content

Repository files navigation

shellr-stock-packs

One sealed pack. One tokenized share. Routed through Voxelithic.

Chain Voxelithic Chain ID Tests

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.


Built with Voxelithic Protocol

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.

1. A ticker is not an identifier

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.

2. The quote answers by reverting

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.

3. A quote goes stale, and stranding the buyer is worse

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.


Two more differences worth knowing

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.


The odds

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 exposure

Nothing 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.


Checking a fill

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.


One thing that is not obvious and cost us a rewrite

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.


Running it

npm install
npm test          # 24 tests, no RPC needed
npm run lineup    # diff the on-chain line-up against LISTED

lineup needs an RPC and STOCK_PACKS_ADDRESS in the environment. No contract address is committed to this repository, so every entry point takes one.

Where the rest lives

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

License

MIT. See LICENSE.

About

Stock packs: one tokenized share per pack, routed through Voxelithic Protocol on Robinhood Chain.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages