Three contracts on Robinhood Chain mainnet.
| What it does | |
|---|---|
ShellrPacks |
Sealed packs of memecoins, paid in ETH or $SHELLR |
ShellrStockPacks |
One random tokenized equity per pack, filled via Voxelithic. Integration layer: shellr-stock-packs |
ShellrStaking |
Stake $SHELLR, earn WETH from the token's Pons creator fees |
ShellrLaunchpad |
Launch a token through pons and tie its fees to a pack |
ShellrFeeVault |
One per launch. Turns that token's fees into pack coins and a burn |
Deployed addresses are not published here. Every consumer - the site, the keeper, the SDK - takes them from its own environment, so a redeploy is one variable rather than a release across four repositories.
Short, and every line of it is money.
- Nothing here is audited.
maxStakeships at 0.25 ETH andpause()exists so that a bad day stays a small bad day. Leave the cap where it is until somebody who audits contracts for a living has read this. - The keeper's key is hot. It lives in Railway's environment. It cannot steal a pack - it can only reveal the seed that was already committed, or fail to, and failing to just refunds the buyer. Give it gas and nothing else.
MASTER_SECRETis the whole fairness story and it does not rotate. Every pack's secret is derived from it. Lose it and every queued commitment becomes unrevealable and every pack in flight refunds. Leak it and anyone can predict what a pack will drop before they buy it - they still cannot change it, but they can wait for a good one. Back it up somewhere that is not the same place as the keeper's key.- The bankroll is real money at risk. Packs that pay more than they cost are
paid out of it. If it empties,
buyrefuses rather than promising a payout the contract cannot make. That refusal is correct behaviour and not an outage.
Robinhood Chain has no Chainlink VRF. We checked against Chainlink's supported networks list - Arbitrum, Avalanche, BASE, BNB, Ethereum, OP, Polygon, Ronin, Soneium, and not this chain. So randomness is commit-reveal across two transactions, which turns out to be better than a VRF for this product anyway: the buyer contributes to the seed, so they do not have to trust an oracle either.
operator chain buyer
──────── ───── ─────
commitSeeds([keccak(s_i)…]) ──► commitments[]
queued BEFORE demand
◄── buy(clientSeed)
pack takes the + ETH
next commitment
at purchase time
reveal(packId, s, minOuts) ──► require(keccak(s) == pack.commitment)
seed = keccak(s, clientSeed, packId, buyer)
payout, count, coins ← seed
swap, deliver
Neither side can steer it. The operator was committed before clientSeed
existed; the buyer never saw s. packId and buyer are in the seed so two
people who pick the same client seed in the same block still get different
packs.
Afterwards, anyone can check it. The secret is in the reveal calldata, the
commitment was on chain before the buy, and the draw is recomputable from
ShellrPacks.sol. @shellr/sdk ships that recomputation as verifyPack, and
the contract itself exposes drawSeed, drawPayout, drawCount and drawPick
as views so you can check it against the deployed bytecode rather than against
our JavaScript.
A pack does not spend the stake. It spends the stake times a multiplier drawn from the same seed, across seven bands. Read the current numbers off the contract rather than out of this file:
payoutBps the chance of each band, in basis points, totalling 10_000
payoutLo the low end of each band's multiplier, in bps of the stake
payoutHi the high end
bandBps the chance of each coin rarity band
10_000 is break even. topPayoutBps() returns the highest multiplier any band
can produce, which is the bankroll's exposure. expectedPayoutBps in
@shellr/sdk computes the mean
those bands imply, so the house edge is derived from live state rather than
quoted from a document that can go stale the next time setPayoutBands is
called.
This makes ShellrPacks a house-banked game rather than a shop. Somebody
has to fund the packs that pay more than they cost; that money is bankroll.
The owner puts it in, winners are paid out of it, losers pay into it. It is
tracked separately from the contract's ETH balance because that balance also
holds buyers' money between buy and reveal, and a pack's stake is never the
bankroll.
A band by the odds in bandBps, then uniformly among the live coins in that
band. An empty band walks to the next one rather than reverting, which is what
keeps packs openable on the day a pool is pulled and the owner marks a coin dead
mid-flight. A dead coin keeps its array index so settled packs still resolve.
The token is not sold. It is taken in, kept, and the pack is funded from the bankroll instead. That is the only way to accept it at all while its liquidity sits in a Pons pool this contract cannot reach.
Be clear about what that is: the bankroll buys the token at a rate the owner sets. A pack paid in ETH only asks the bankroll for the difference when it wins; a pack paid in token asks for the whole spend. Three brakes, all of them load-bearing:
tokenPerEth- the rate, set by hand because there is no pool here to read one from. Stale in one direction is a gift.tokenDailyCap- a ceiling on how much ETH the bankroll hands out this way per day, reset on its own.tokenSales- an off switch that touches nothing else.
Anyone can launch a token on this chain in one click; pons already does that well and there is no reason to rebuild it. What a launch does not normally have is a reason for anybody to hold the token after the first week.
So the launchpad does not create tokens. It creates them through pons and redirects one thing: where the creator's fees go.
creator ShellrLaunchpad pons v2
─────── ─────────────── ───────
launch(params, pack) ──► deploy ShellrFeeVault
params.creatorFeeRecipient = vault
params.creatorTaxBps = 0
|
└──────────────────► launchToken()
deploys token
opens the curve
vault.bindToken(token) ◄───── returns token
...trading...
every trade pays pons' 1%
pons keeps its protocol share
the rest credits the vault in PonsV2FeeEscrow
|
keeper ──────────────► vault.sweep()
claim() from the escrow
20% buys $SHELLR and destroys it
80% buys the pack's coins
|
└──► distributor, for holders to claim
creatorTaxBps goes out as zero, always, and the launchpad overwrites
whatever the caller put there. pons lets a creator charge an extra tax on every
trade; using it would make a token launched here more expensive to trade than
the identical token launched anywhere else, and a token that is worse to trade
is a token that does not trade.
What gets redirected is the creator's existing share of pons' standard fee. The money already existed. The launchpad only changes where it lands.
ShellrFeeVault is deployed per token rather than shared. A shared vault would
have to attribute every incoming payment to the token that produced it, and
pons' escrow credits a recipient, not a reason. One contract per launch makes
the accounting a fact about the address rather than a ledger we maintain.
The vault is also what pons is handed as creatorFeeRecipient, which is why it
has to exist before the launch call - and why the token is bound to it
afterwards rather than predicted. The factory's comments point at a
predictLaunchAddresses helper for computing the CREATE2 address in advance;
that function is not in the published deployer, and recomputing the address here
would pin this contract to the exact creation code of theirs. bindToken is one
storage write and depends on nothing that can move.
A launch copies the pack's coins into its vault. A pack edited next week does not change what a token launched today buys, because its holders bought into the line-up that was published with it.
sweep takes the swap minimums as arguments, for the same reason
ShellrStockPacks.reveal does: the quoter answers by reverting, which is fine
from a staticcall and useless inside a transaction that carries on afterwards.
So the keeper quotes off chain and passes the results in.
A single failed pool reverts the whole sweep on purpose. A partial sweep would credit holders for a pack missing a coin while the fees that should have bought it were already spent; reverting leaves the money claimable and lets the keeper retry once the pool is fixed.
pons v1 tokens are a plain ERC-20 and cannot burn themselves; v2 tokens are
ERC20Burnable. The vault tries burn(uint256) and falls back to transferring
to the dead address. Both remove supply from circulation, only one lowers
totalSupply, and nothing downstream depends on which. $SHELLR predates v2, so
in practice it takes the second path - and without the fallback every sweep
would have reverted.
The distributor is an address the vault pays into, and the contract that hands those coins to holders is not written yet. Snapshot and claim, so that passive holders get their share without having to stake anything. Until it exists, bought coins accumulate at the distributor address and go nowhere.
src/
ShellrPacks.sol the packs, the draw, the bankroll, the commit queue
ShellrStockPacks.sol one tokenized equity per pack, via Voxelithic's router
ShellrStaking.sol stake $SHELLR, earn WETH
ShellrLaunchpad.sol launch through pons, tie the fees to a pack
ShellrFeeVault.sol one per launch: claim, split, buy, burn
test/
ShellrPacks.t.sol draw vectors, bankroll accounting, refund window
ShellrStockPacks.t.sol symbol weighting, router failure paths
ShellrLaunchpad.t.sol the fee recipient is ours, the creator tax is zero
ShellrFeeVault.t.sol the split, the remainder, the burn fallback
lib/ is not committed.
forge install foundry-rs/forge-std
forge testIf forge cannot reach binaries.soliditylang.org for the compiler, fetch it
from the solidity GitHub releases and point at it directly:
forge test --use /path/to/solc-0.8.26forge test -vv asserts the same draw vectors that
shellr-sdk and
shellr-keeper pin on their
side. All three have to agree. If they drift, reveals revert on the first
slippage check and every pack stalls until its refund window - loudly, which is
the one mercy in that failure.
Foundry is optional here. The deploy path lives in
shellr-keeper and runs on
Node: it compiles with the solc npm package and sends the creation
transaction with viem, which is what forge create does in three steps rather
than one. Foundry gives you forge test and source verification on the
explorer, and nothing else in the sequence needs it.
The order matters and the runbook is in the keeper's README. Briefly:
npm run pools- find out which coins actually have a v3 pool against WETH. A coin that printsNO POOLcannot be in a pack, and finding that out here rather than after deploy is the point of the step.npm run check- the keeper's arithmetic against the contract's. If they differ, every reveal reverts.npm run deploy- dry run, read every constructor argument, thenCONFIRM=1.npm run configure- registers each coin against its deepest pool.- Start the keeper.
buyreverts withno seeds queueduntil the first batch of commitments lands, which is correct and not a bug. - Buy one pack at the minimum from a wallet that is not the owner's. Watch it
reveal. Check the explorer that the coins reached the buyer and the fee
reached the treasury. Then consider raising
maxStake.
Turning it off:
npm run pause # stops new buys and sell-backs
npm run pause -- offPausing does not stop reveals or refunds, on purpose. Packs already paid for still have to settle.
MIT. See LICENSE.