Documentation
Everything Wick does, and everything that was checked
A wick is a standing order: spend this many dollars on this stock, this often, until this budget is gone. It lives in one contract of 8,675 bytes that holds nothing, has no owner and cannot be changed. This page is what it does, what stops a stranger abusing it, and every check that was actually run — with the numbers read out of the files those runs wrote.
The contract
| Address | 0xfbCf…f7eE |
| Deployed by | whoever lights the first wick, through the standard CREATE2 deployer |
| State | not deployed yet — the first person to light a wick deploys it, in the same transaction |
| Compiler | solc 0.8.26, optimizer 2000 runs |
| Runtime size | 8,675 bytes |
| Init code | 9,701 bytes |
| Functions that change anything | light(), pull(), snuff() |
| Runtime code hash | 0x7a2984393aa2c3f589fb002d5d3d9dd15134bb0dc400081ae2deaf30a14b35cd |
The address is a function of the code: change one byte of the source and it moves. So the address above is a claim about the code you are calling, and tools/validate.mjs recompiles the source on every build and refuses to publish a page whose address, init hash or runtime hash has drifted.
The six things it can do
| light | records a schedule. Moves no money. |
| pull | draws one period's dollars, buys, pays the caller a tip. Anyone may call it. |
| snuff | ends a wick. Only its owner. |
| order | reads one schedule back. |
| status | is it due, what would it draw, is the owner short, what minimum would it demand. |
| minRawOut | the guard itself, as a view: what a given number of dollars must buy. |
What it does not have
No owner. No admin role. No pause. No upgrade path, no proxy, no delegatecall and no selfdestruct — a bytecode scan asserts that, with a control that finds one deliberately planted in a two-line contract. No fee switch: the tip is fixed at construction and the constructor refuses a tip above 100 bps, so reading the source is enough to know nobody can deploy a greedier Wick at this address.
The rules, in the order pull() applies them
| Rule | What it stops | If it fires |
|---|---|---|
| the wick must be live | drawing on a snuffed or finished schedule | NotLive |
| now ≥ the due time | pulling early, or twice in one period | NotDue |
| the draw is always the whole amount | a part-draw too small to be worth a tip | light() refuses a budget that is not a whole number of buys |
| the tip is bps of the draw, floored and capped | a tip below the caller's gas, and a big draw paying a big tip for the same work | the floor (5c) or the cap (25c) |
| the feed must answer, positively | buying against a dead oracle | BadPrice |
| the answer must be fresh | buying against a price nobody has updated | StalePrice |
| the schedule advances before any money moves | re-entering to draw twice | nothing to re-enter into |
| a lapsed wick re-arms from now | a forgotten wick becoming four buys at one price | one draw, then a period from today |
| the caller is paid last, explicitly | a caller taking anything but the tip | one transfer, of the tip, to msg.sender |
| the minimum comes from the feed | a caller routing you through a bad pool | Uniswap refuses the swap |
| the recipient is the owner | the shares going anywhere else | it is a constant in the code |
And one rule outside the contract: Wick can only ever move what your ERC-20 allowance lets it move. Revoking that allowance in your wallet stops every wick you have, immediately, without Wick's cooperation — which is the definition of not being custodial.
What was checked, and how
Three suites. Every number below is read out of the JSON the run wrote.
22/22 properties, executed on Robinhood Chain
Each one is a single eth_call with no to: a harness runs against the real Uniswap router, the real dollar and the real tokenized stock, with Wick's own runtime installed at its own address by a state override, and three fresh addresses funded at the storage slot the tools found for that token. Nothing is deployed and nothing is spent. Every refusal is classified by the error it returned — an unrecognised one fails the property, because “something said no” is how a guard takes credit for a failure that had nothing to do with it.
| # | Property | Result |
|---|---|---|
| 1 | lighting a wick moves no money at all, and the schedule stored is the schedule asked for | pass |
| 2 | a due wick draws exactly the amount, buys at least the minimum, and the shares land with the owner | pass |
| 3 | the tip is 20 bps of the draw held between a 5-cent floor and a 25-cent cap: 5 cents on a $25 draw (the floor and the rule agree), 25 rather than 200 on $1,000 | pass |
| 4 | a second pull inside the period is refused (control: the first, in the same call, went through) | pass |
| 5 | a snuffed wick is refused (control: its twin, not snuffed, pulls in the same call) | pass |
| 6 | only the owner can snuff (control: the owner then snuffs the same wick) | pass |
| 7 | with the allowance revoked the pull is refused by the dollar itself (control: restored, it pulls) | pass |
| 8 | the price guard binds: at a feed price the pool cannot meet, the pull is refused (control: at the true price it goes through) | pass |
| 9 | the caller picks the pool and the owner's tolerance judges it: Apple's 1% pool is refused, its 0.05% pool goes through | pass |
| 10 | the minimum is computed from the feed: it matches a reference built without the page's module | pass |
| 11 | a dead feed is refused with BadPrice, and a stale one with StalePrice (control: the live mock pulls) | pass |
| 12 | a budget must be a whole number of draws: one that is not is refused with BadArg, and the wick that is goes through | pass |
| 13 | light() refuses a wick that is malformed, one reason at a time (control: the well-formed one is lit) | pass |
| 14 | shares are counted the way the token counts them: the minimum agrees with balanceOfUI | pass |
| 15 | CONTROL: dollars written to the wrong storage slot fund nothing, and the harness says so | pass |
| 16 | Wick holds nothing: after a pull its dollar and share balances are both zero | pass |
| S1 | ABI surface: the contract exposes exactly three state-changing functions and no others | pass |
| S2 | ABI surface CONTROL: an allowlist that omits a function which exists must fail | pass |
| S3 | ABI surface: there is no owner, admin, pause, upgrade or fee setter anywhere in the ABI | pass |
| S4 | ABI surface: the runtime code contains no DELEGATECALL, CALLCODE or SELFDESTRUCT | pass |
| S5 | ABI surface CONTROL: the same scan finds a DELEGATECALL that is deliberately there | pass |
| S6 | the published address is the CREATE2 address of the published init code, and has no code yet | pass |
7/7 properties that need a clock
Time cannot move inside one eth_call, and almost everything a person is trusting a schedule with only exists over time. These run on a fork of the chain that this run starts and kills, sending real transactions weeks apart.
| # | Property | Result |
|---|---|---|
| T1 | a wick re-arms exactly one period later, and a second before that it is still NotDue a second early it is NotDue; at the second it is due it goes through (period 7 days) | pass |
| T7 | CONTROL: the clock really moves — a pull refused as NotDue goes through once the period passes the clock moves and the refusal was about the clock | pass |
| T5 | the schedule the chain stores is the schedule the page's own module predicts, at every step 3 schedule steps agree with js/schedule.js to the second, including one pulled a day late | pass |
| T2 | a lapsed wick does not catch up: pulled five weeks late it draws once and re-arms from now pulled 35.0 days late it drew $25 once and re-armed 7 days from now, and the event recorded how late it was | pass |
| T3 | a budget runs out exactly: every draw is the full amount, the wick closes on the last one, and a further pull is NotLive 3 draws spent exactly $75, each one $25 with a $0.05 tip, then the wick closed and a further pull is NotLive | pass |
| T6 | over a whole budget, Wick never holds a dollar or a share across every draw, Wick's dollar and share balances were zero every time they were read | pass |
| T4 | a feed that goes stale stops the wick with StalePrice, and a fresh answer starts it again 36 hours without an answer and the pull is StalePrice; one fresh answer later it goes through | pass |
Random sequences, and then 13/13 deliberate breaks
A property builds one state and asserts one thing; bugs live in sequences. The fuzz sends 30 random operations per seed — lighting, pulling, snuffing, waiting anything from an hour to two months, moving the price, revoking the allowance — and checks 11 invariants after every one. Every seed then ends with a scripted walk down the whole path, because a random walk is not coverage: it can finish without ever lapsing a wick, running a budget out, paying a tip at the cap or having a pull refused, and a break that only shows in one of those then reads as uncaught.
The invariants
| I1 | no wick ever draws more than the budget it was lit with |
| I2 | no wick is ever drawn twice inside one period |
| I3 | the order the chain stores is the order the model expects, field by field |
| I4 | Wick holds no dollars and no shares, ever |
| I5 | every owner's dollars fell by exactly the sum of their own draws |
| I6 | every caller's dollars rose by exactly the sum of the tips they earned |
| I7 | every draw bought at least the minimum the feed implied, computed independently |
| I8 | every refusal is a declared error, and one the model agrees with |
| I9 | COVERAGE: the sequence actually pulled, lapsed, ran a budget out and paid a tip at the cap |
| I10 | the price guard: every landed pull bought at least the minimum the feed implied, and a pool that cannot meet it is refused |
| I11 | light() accepts only a well-formed schedule: a budget that is not a whole number of draws is refused, and the same wick with a whole one is lit |
The breaks
Each is one line of the contract changed, recompiled, and every seed replayed. 13 were caught by the invariant named for them.
| # | What was broken | Seeds | Caught by |
|---|---|---|---|
| B1 | the schedule never re-arms (nextAt is left where it was) | 3/3 | I3 |
| B2 | a lapsed wick catches up (nextAt += period even when that is in the past) | 3/3 | I3 |
| B3 | the budget is never spent down | 3/3 | I3 |
| B4 | the tip is not capped | 3/3 | I6 |
| B13 | the tip has no floor, so a small draw pays a caller less than their gas | 3/3 | I6 |
| B5 | the shares are sent to whoever pulled, not to the owner | 3/3 | I7 |
| B6 | the minimum is thrown away, so any pool is acceptable | 3/3 | I10 |
| B7 | anyone may snuff anyone's wick | 3/3 | I3 |
| B8 | a spent wick is not closed | 3/3 | I3 |
| B9 | a budget that is not a whole number of draws is accepted | 3/3 | I11 |
| B10 | a wick may be pulled the moment it is lit and again immediately | 2/3 | I8 |
| B11 | the price is read with the wrong scale (a factor of a million) | 3/3 | I10 |
| B12 | the tip is taken on top of the draw rather than out of it | 3/3 | I8 |
The fuzzer caught 13 of 13 deliberate breaks, 13 of them by the invariant named for it. The two that got through the first time were an uncapped tip — the fuzz was only drawing $5-$60, where 20 bps is a penny and the 25-cent cap never binds — and a weakened price minimum, which no invariant could see while every pull happened to use a good pool. Both are now checks of their own: draws large enough for the cap to decide the tip, and a leg that pulls through the 1% pool and requires a refusal.
The market scan
This chain carries hundreds of tokens that copy the names of real ones, so nothing in the directory starts from a name. Candidates come from two sibling scans that began at the one real dollar address; then, at one pinned block, every token must answer symbol and decimals, every pool must be the address Uniswap's own factory returns for that pair and fee, and every price feed must be the feed this chain's own lending market names for that token — and must describe itself as that stock. A stock is listed only when its best pool fills a $25 buy within 300 bps of that feed.
| Stock | Price | $5 | $25 | $100 | $1,000 | Pools |
|---|---|---|---|---|---|---|
| SGOV iShares 0-3 Month Treasury Bond | $101.11 | -58 bps | -58 bps | -58 bps | -58 bps | 2 |
| AMD AMD | $559.42 | -51 bps | -51 bps | -50 bps | -43 bps | 2 |
| GME GameStop | $22.55 | -19 bps | -18 bps | -18 bps | -17 bps | 3 |
| PLTR Palantir Technologies | $177.60 | -16 bps | -16 bps | -15 bps | -13 bps | 1 |
| NVDA NVIDIA | $222.45 | -13 bps | -13 bps | -13 bps | -13 bps | 2 |
| ASML ASML Holding NV | $1,676.63 | -12 bps | -10 bps | -6 bps | +51 bps | 2 |
| MU Micron Technology | $1,014.43 | -4 bps | -4 bps | -4 bps | -3 bps | 1 |
| AAPL Apple | $335.38 | 0 bps | 0 bps | 0 bps | +1 bps | 3 |
| MSFT Microsoft | $495.82 | +11 bps | +11 bps | +11 bps | +12 bps | 1 |
| GOOGL Alphabet Class A | $350.47 | +23 bps | +23 bps | +23 bps | +23 bps | 2 |
| SPY SPDR S&P 500 ETF Trust | $761.55 | +26 bps | +26 bps | +26 bps | +27 bps | 2 |
| TSM Taiwan Semiconductor Manufacturing | $434.26 | +31 bps | +32 bps | +32 bps | +37 bps | 1 |
| CRCL Circle Internet Group | $91.76 | +37 bps | +37 bps | +37 bps | +38 bps | 1 |
| USAR USA Rare Earth | $15.61 | +37 bps | +37 bps | +37 bps | +45 bps | 1 |
| TSLA Tesla | $363.80 | +45 bps | +45 bps | +47 bps | +58 bps | 3 |
| AMZN Amazon | $253.86 | +48 bps | +48 bps | +48 bps | +49 bps | 1 |
| SNDK Sandisk Corporation | $1,792.32 | +51 bps | +52 bps | +53 bps | +68 bps | 1 |
| SLV iShares Silver Trust | $59.85 | +54 bps | +73 bps | +75 bps | +76 bps | 3 |
| META Meta Platforms | $666.76 | +85 bps | +85 bps | +85 bps | +87 bps | 2 |
| INTC Intel | $109.05 | +95 bps | +95 bps | +96 bps | +102 bps | 1 |
| BABA Alibaba | $113.77 | +110 bps | +110 bps | +111 bps | +117 bps | 2 |
| DELL Dell | $570.72 | +143 bps | +143 bps | +143 bps | +147 bps | 1 |
And the 23 that were dropped
Worth reading: 10 of these are dropped because the only price for them on this chain is a Uniswap pool's own price. A pool cannot be the reference for a guard on a trade in that pool — the check would be the thing it is checking — so Wick will not buy them at all, rather than buying them with a guard that means nothing.
| Stock | Why not |
|---|---|
| AMC | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name AMC |
| COST | the Morpho oracle for this token names no price feed |
| DJT | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name DJT |
| GLD | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name GLD |
| HIMS | the Morpho oracle for this token names no price feed |
| IBM | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name IBM |
| LLY | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name LLY |
| LULU | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name LULU |
| MRNA | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name MRNA |
| NFLX | the Morpho oracle for this token names no price feed |
| QQQ | the Morpho oracle for this token names no price feed |
| RBLX | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name RBLX |
| RDDT | the Morpho oracle for this token names no price feed |
| RIVN | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name RIVN |
| SPCX | the Morpho oracle for this token names no price feed |
| TTWO | its only on-chain price is "Uniswap V3 Pool Price in USD", which does not name TTWO |
| USO | the Morpho oracle for this token names no price feed |
| COIN | no v3 pool that Uniswap's factory confirms |
| EWY | no v3 pool that Uniswap's factory confirms |
| NBIS | no v3 pool that Uniswap's factory confirms |
| ORCL | no v3 pool that Uniswap's factory confirms |
| RKLB | no v3 pool that Uniswap's factory confirms |
| MSTR | cheapest $25 buy costs 410 bps over the feed, above the 300 bps bar |
What it costs
| Transaction | Gas | At today's price |
|---|---|---|
| Light a wick (approve + light) | 231,292 | 4.00c |
| Pull a due wick | 266,417 | 4.61c |
| Snuff a wick | 32,799 | 0.57c |
| Deploy Wick (once, by the first person) | 1,927,440 | 33.36c |
Measured on a fork of Robinhood Chain at block 67,536,563, at the 0.0660 gwei the chain reported and the $2,624 its own ETH/USD feed reported. Deploying the contract — once, by whoever is first — is 33.36c more.
The tip is the only thing Wick itself charges, and it does not go to us: it goes to whoever sent the transaction that bought your shares. There is no protocol fee, no treasury and no token.
What could still go wrong
Nobody pulls it
The tip is small on a small draw. If nothing in the queue is worth anyone's gas, your wick simply waits — and you can pull it yourself from the app, which is one transaction and costs you the gas you would have paid anyway.
Robinhood pauses a stock
These tokens have an admin, a blocklist and a pause, held by their issuer. A paused token cannot be bought and the pull is refused. Wick cannot fix that and does not pretend to.
The feed is wrong
The guard is only as good as Robinhood's price. If the feed is wrong, the minimum is wrong. The feed for every listed stock is the same one this chain's lending market uses to decide whether to liquidate people, which is the strongest thing that can be said for it.
The contract has a bug
It has never been audited. It is small, it holds nothing, and the worst thing a bug could do is spend the allowance you gave it on the stock you named — which is a reason to make the budget the amount you are willing to lose track of, and no more.
The pool moves between quote and pull
It will. That is what the minimum is for: the fill can be better than the quote, and if it would be worse than your limit the transaction is refused and nothing happens.
You forget it is running
A wick has a budget and an end date, both shown before you sign, and both are enforced by the contract. That is the answer to “an allowance you forget”: it can only ever spend what you already decided.