How a creel works, and what was checked.
One contract, 276 lines, 6,520 bytes of runtime. It has no owner, no fee, no oracle, no pause and no upgrade path, and it is not audited.
The contract
A creel is an ERC-20 whose backing is a fixed number of RAW token units per whole share. Five things go into its constructor — the name, the symbol, the Permit2 it trusts, the tokens and the units — and nothing can change afterwards, because there is no function that changes any of them.
Minting
mint(shares, to) pulls ceil(units[i] × shares ÷ 1e18) of every leg from the caller and mints shares. mintWithPermit2 does the same through one Permit2 batch signature, so the only spender you ever authorise is Uniswap's Permit2 and never a contract of ours. The rounding is up, deliberately: a mint that does not divide evenly leaves the creel over-backed by a unit rather than short by one.
Redeeming
redeem(shares, to) burns the shares and sends floor(units[i] × shares ÷ 1e18) of every leg. Rounding down, for the same reason. redeemWaiving(shares, to, takeMask) takes only the legs whose bit is set; the rest stay behind as backing for the shares still outstanding, which can only raise the backing per share.
The invariant
At the end of every mint and every redemption the contract checks, on chain, that for every leg balanceOf(creel) ≥ ceil(totalSupply × units ÷ 1e18), and reverts if it is not true. It is the only thing the contract insists on, and everything else follows from it.
Where it lives
Through Arachnid's CREATE2 factory at 0x4e59b44847b379578588920cA78FbF26c0B4956C, with a salt of keccak256(symbol). The address is therefore a hash of the init code, which contains the composition — so the same basket always has the same address, for everybody, and no other code can ever sit there. The first person to mint a given creel deploys it from their own wallet. This project holds no key and has never broadcast a transaction to this chain.
- solc
- 0.8.26
- creation code
- 8,208 bytes · 0x2ae8b9b89f5b9bb024a7c7efeba7e52a43ddd5684d50f725fc5d6eb12430432e
- runtime
- 6,520 bytes · 0x08f0c9b91015621abdb2418fb89458418a5e34acce6f3c41017afa4e82cd7096
What this chain actually offers
Measured on Sep 20, 2026 at block 68,291,455, not looked up.
- 45 tokenized stocks, every one of them 18 decimals, none paused at the time of the scan.
- 28 have a Robinhood price feed and 17 do not (AVGO, NFLX, COST, RDDT, HIMS, RBLX, MRNA, LLY, DJT, QUBT, TTWO, NU, RIVN, SOUN, AMC, JNJ, NET). The feeds were found by arithmetic rather than from a list: a known feed was created by 0xfE3c…74d8, an ordinary account's contracts sit at keccak(rlp([deployer, nonce])), so all 942 of that account's addresses were derived and asked what they were. 172 answered.
- 26 can also be bought through a Uniswap v3 pool that quotes, and those are the only ones a creel may hold.
- 14 carry a uiMultiplier above 1, the largest being SGOV at 1.005102 (0.51%). Raw units never move, so a creel collects that automatically — and the same mechanism can be set down, which is a risk of holding the stock at all.
What was checked
Properties, on the live chain
20 of 20, with 164 assertions. Each is one eth_call with no to: the harness's creation code runs against the real Robinhood stock tokens and the real Permit2, puts the creel on chain through the real CREATE2 factory, and returns a verdict. Nothing is deployed and nothing is signed. The control — the same property with the wallet funding removed — was refused.
- the creel lands at the address the site publishes · 2 assertions
- the composition on chain is the composition on the page · 18 assertions
- minting moves exactly the units the quote named, and mints exactly the shares asked for · 17 assertions
- minting rounds up: a share count that does not divide leaves the creel over-backed, never short · 14 assertions
- redeeming pays exactly the units the quote named and burns exactly the shares · 9 assertions
- a round trip never returns more of any leg than it took · 7 assertions
- backing holds through a mint, a transfer and a partial redemption · 8 assertions
- redeeming more than you hold is refused as InsufficientBalance — and what you do hold is not · 3 assertions
- a mint one unit short of a single leg is refused by that leg · 3 assertions
- a waiver leaves the waived leg behind and raises the backing behind it · 9 assertions
- zero shares, zero redemption and a waiver that takes nothing are each refused by name · 3 assertions
- a change in a stock’s uiMultiplier changes what a creel is worth, not what it holds · 2 assertions
- the EIP-712 digest the page signs is the one Permit2 checks · 1 assertions
- a fresh creel holds nothing, and a full cycle leaves nothing behind · 15 assertions
- shares are an ordinary ERC-20, and an empty holder cannot send one · 3 assertions
- a creel whose legs are not strictly ascending cannot be built at all, while a well-formed one can · 2 assertions
- a Permit2 batch naming the wrong tokens is refused before anything moves, and a correct one gets past that check · 3 assertions
- a redemption pays the address it was told to pay, not the caller · 14 assertions
- the page’s own arithmetic is the contract’s arithmetic, to the unit · 15 assertions
- a holder who owns none of the underlying can still redeem · 16 assertions
The page's own transaction, against the real pools
3 of 3. The transaction the app would send is built by js/route.js and handed to a harness as bytes, which runs it against the live Universal Router and the live pools inside one eth_call. Buying 7 legs cost $20.04 at quote. The control — the same run funded at a storage slot that is not the balances mapping — was refused.
- the page’s own transaction buys exactly the units it quoted, for no more than it said · 25 assertions
- what the buy delivers is exactly what the creel then pulls — the two steps compose to the wei · 16 assertions
- a per-leg ceiling below what the pool wants is refused by the router, not by something else · 2 assertions
The sabotage sweep
16 of 18 deliberate breaks in the contract and in the page's own modules were caught by a property NAMED for them — attribution, not "the suite went red", because a suite that goes red for its own reasons takes credit for everything. Every break that survived is declared below with the reason it is invisible, and the tool checks those declarations against the sweep's own output.
- s1 — minting rounds DOWN instead of up, so a share count that does not divide leaves the creel a unit short · caught by minting rounds up: a share count that does not divide leaves the creel over-backed, never short
- s2 — the redemption QUOTE rounds up while the redemption itself rounds down, so the app promises a unit more than it pays · caught by redeeming pays exactly the units the quote named and burns exactly the shares
- s3 — redemption pays the ROUNDED-UP amount and the backing check is deleted — both guards at once, because either one alone catches the other · caught by backing holds through a mint, a transfer and a partial redemption
- s4 — redeemWaiving ignores the mask and pays every leg anyway · caught by a waiver leaves the waived leg behind and raises the backing behind it
- s5 — minting mints one share more than it was paid for · caught by minting moves exactly the units the quote named, and mints exactly the shares asked for
- s6 — redemption pays out but forgets to burn the shares · caught by redeeming pays exactly the units the quote named and burns exactly the shares
- s7 — transfer credits the recipient without debiting the sender · caught by shares are an ordinary ERC-20, and an empty holder cannot send one
- s8 — minting pulls the units from the recipient instead of from the caller · caught by minting moves exactly the units the quote named, and mints exactly the shares asked for
- s9 — the constructor stops requiring that the legs ascend, so a basket can hold the same stock twice and have a second address · caught by a creel whose legs are not strictly ascending cannot be built at all, while a well-formed one can
- s10 — the backing check reads the CALLER’s balance instead of the creel’s · SURVIVED: invisible while the caller happens to hold plenty of the underlying, which every other property arranges. The property written for it (s10b, "a holder who owns none of the underlying can still redeem") empties the wallet first and catches it.
- s11 — mintWithPermit2 stops checking that the permit names this creel’s own legs · caught by a Permit2 batch naming the wrong tokens is refused before anything moves, and a correct one gets past that check
- s12 — redemption pays the caller rather than the address it was given · caught by a redemption pays the address it was told to pay, not the caller
- s13 — a waiver that takes no leg at all is allowed, so shares can be burned for nothing · caught by zero shares, zero redemption and a waiver that takes nothing are each refused by name
- s14 — the PAGE quotes a mint rounded down, so the app shows less than the transaction will actually take · caught by the page’s own arithmetic is the contract’s arithmetic, to the unit
- s15 — the swap path is written in exact-INPUT order, which is backwards for an exact-output swap · caught by the page’s own transaction buys exactly the units it quoted, for no more than it said
- s16 — the SWEEP at the end of the command list is dropped, so unspent dollars are left in the router · SURVIVED: removing the SWEEP changes nothing observable: every leg is an exact-output swap, which pulls exactly what it needs, so the router never holds a unit for the sweep to return. The command is kept as a backstop, not because a test can see it.
- s17 — the per-leg ceiling is a million times the quote, so one thin pool could eat the whole budget · caught by a per-leg ceiling below what the pool wants is refused by the router, not by something else
- s10b — the backing check reads the CALLER’s balance instead of the creel’s - the same break as s10, now caught by the property written for it · caught by a holder who owns none of the underlying can still redeem
The fuzz
4 random sequences, 91 operations in all, with 10 invariants checked after every single one — mint, redeem, waive, transfer, pause a stock, move a multiplier — each seed ending in a fixed coda that forces the operations the dice may have skipped. Then 8 deliberately broken builds replayed against every seed: 7 were caught. The ones that were not are named here: f1 (deleting the backing check from redemption alone changes nothing: redemption already rounds DOWN, so the creel can never be left short by it and the check has nothing to catch. It is the second of two guards. The sabotage sweep pairs this deletion with a redemption that rounds UP (s3), and that pair is caught.).
The whole thing, in a browser
25 of 25. A fork of this chain, the actual files served the way Vercel serves them, Chrome, and a stub wallet holding a key that can only reach the fork — then the site's own buttons: buy the parts, approve Permit2, deploy the creel, mint, pause a stock on the fork and redeem around it, and send the whole basket as one transfer.
- the buyer holds dollars on the fork · $50000
- the fork’s pools are warm · 6 quotes in 14s
- the stub wallet is installed and is the buyer · a fresh key on the fork
- the page is talking to the fork this run started · 10.0 ETH
- nobody has deployed this creel yet · a fresh key on the fork
- the app bought the parts in one transaction · 0xa7594548d6457239305c6e0756db0c5405456860c559b18576d5b15b34a1ade4
- every leg arrived to the wei — an exact-output buy · SLV 66828168073000000 GLD 14970915137000000
- it spent dollars, and not many · $10.023243
- approved 2 stocks to Permit2 · the only spender the app ever asks for
- the creel landed at the address the site publishes · 6520 bytes at a fresh key on the fork
- and its runtime is exactly what this build’s init code stores · 0xf567af0beb…
- the app minted with one signature and one transaction · 0x4b90652e9ca5660c67141d9fa401c661f82a860a6dd8dd435378998b3cdcc232
- one whole share arrived · 1.0
- and the wallet has no stock left over — the buy and the mint composed exactly · 0,0
- the creel is backed for every leg · SLV 66828168073000000/66828168073000000 GLD 14970915137000000/14970915137000000
- SLV is paused on the fork · exactly as Robinhood could pause it
- a redemption that insists on every leg is refused while one is paused · execution reverted (unknown custom error) (action="call", da
- the app unticks the paused leg by itself · SLV is left behind unless you insist
- the app redeemed around the paused stock · 0xac4c04d28d4f42ac0a028400a3ff437b079e7d06a7fa03594d1481f1782b1955
- the waived leg (SLV) did not move · 66828168073000000
- and the legs that could move did (GLD) · 5988366054800000
- the shares were burned · 0.6
- waiving raised the backing behind the leg that was left · 66828168073000000 held against 40096900843800000 required
- the whole basket moved in one transfer · 0xe7d97fb124fcbfcfe4edf76a8857a517f2d995381ea619402c7ab71df8cd5bf7
- the friend holds the creel now · 0.6
What is not done
- It is not audited. It is new code written in one sitting. The properties, the sweep and the fuzz are what there is, and they are all in this repository.
- The way in uses Uniswap v3 only. v4 is quoted for comparison and never traded. Across 39 names where both venues quoted, that costs a median of 0.0 bp — and up to 73.3 bp on the worst one.
- There is no pool for a creel token. The way out is to redeem it and sell the parts, which is the way it is meant to work; but it does mean a creel is not a thing you can sell in one click to somebody who wants one.
- A creel never rebalances. That is the design, not an omission, and it means the weights you see on the front page are the weights at the block the creel was struck at.
- Robinhood can pause a stock or blocklist an address. The waiver is what stops that freezing a basket, but a paused leg is still a leg you cannot take out today.
- $25,000 through the pools costs a median of 19.4 bp and up to 2,458 bp. Size matters here more than anything else on this page.