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

Address0xfbCf…f7eE
Deployed bywhoever lights the first wick, through the standard CREATE2 deployer
Statenot deployed yet — the first person to light a wick deploys it, in the same transaction
Compilersolc 0.8.26, optimizer 2000 runs
Runtime size8,675 bytes
Init code9,701 bytes
Functions that change anythinglight(), pull(), snuff()
Runtime code hash0x7a2984393aa2c3f589fb002d5d3d9dd15134bb0dc400081ae2deaf30a14b35cd

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

lightrecords a schedule. Moves no money.
pulldraws one period's dollars, buys, pays the caller a tip. Anyone may call it.
snuffends a wick. Only its owner.
orderreads one schedule back.
statusis it due, what would it draw, is the owner short, what minimum would it demand.
minRawOutthe 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

RuleWhat it stopsIf it fires
the wick must be livedrawing on a snuffed or finished scheduleNotLive
now ≥ the due timepulling early, or twice in one periodNotDue
the draw is always the whole amounta part-draw too small to be worth a tiplight() refuses a budget that is not a whole number of buys
the tip is bps of the draw, floored and cappeda tip below the caller's gas, and a big draw paying a big tip for the same workthe floor (5c) or the cap (25c)
the feed must answer, positivelybuying against a dead oracleBadPrice
the answer must be freshbuying against a price nobody has updatedStalePrice
the schedule advances before any money movesre-entering to draw twicenothing to re-enter into
a lapsed wick re-arms from nowa forgotten wick becoming four buys at one priceone draw, then a period from today
the caller is paid last, explicitlya caller taking anything but the tipone transfer, of the tip, to msg.sender
the minimum comes from the feeda caller routing you through a bad poolUniswap refuses the swap
the recipient is the ownerthe shares going anywhere elseit 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.

#PropertyResult
1lighting a wick moves no money at all, and the schedule stored is the schedule asked forpass
2a due wick draws exactly the amount, buys at least the minimum, and the shares land with the ownerpass
3the 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,000pass
4a second pull inside the period is refused (control: the first, in the same call, went through)pass
5a snuffed wick is refused (control: its twin, not snuffed, pulls in the same call)pass
6only the owner can snuff (control: the owner then snuffs the same wick)pass
7with the allowance revoked the pull is refused by the dollar itself (control: restored, it pulls)pass
8the price guard binds: at a feed price the pool cannot meet, the pull is refused (control: at the true price it goes through)pass
9the caller picks the pool and the owner's tolerance judges it: Apple's 1% pool is refused, its 0.05% pool goes throughpass
10the minimum is computed from the feed: it matches a reference built without the page's modulepass
11a dead feed is refused with BadPrice, and a stale one with StalePrice (control: the live mock pulls)pass
12a budget must be a whole number of draws: one that is not is refused with BadArg, and the wick that is goes throughpass
13light() refuses a wick that is malformed, one reason at a time (control: the well-formed one is lit)pass
14shares are counted the way the token counts them: the minimum agrees with balanceOfUIpass
15CONTROL: dollars written to the wrong storage slot fund nothing, and the harness says sopass
16Wick holds nothing: after a pull its dollar and share balances are both zeropass
S1ABI surface: the contract exposes exactly three state-changing functions and no otherspass
S2ABI surface CONTROL: an allowlist that omits a function which exists must failpass
S3ABI surface: there is no owner, admin, pause, upgrade or fee setter anywhere in the ABIpass
S4ABI surface: the runtime code contains no DELEGATECALL, CALLCODE or SELFDESTRUCTpass
S5ABI surface CONTROL: the same scan finds a DELEGATECALL that is deliberately therepass
S6the published address is the CREATE2 address of the published init code, and has no code yetpass

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.

#PropertyResult
T1a 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
T7CONTROL: 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
T5the 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
T2a 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
T3a 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
T6over 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
T4a 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

I1no wick ever draws more than the budget it was lit with
I2no wick is ever drawn twice inside one period
I3the order the chain stores is the order the model expects, field by field
I4Wick holds no dollars and no shares, ever
I5every owner's dollars fell by exactly the sum of their own draws
I6every caller's dollars rose by exactly the sum of the tips they earned
I7every draw bought at least the minimum the feed implied, computed independently
I8every refusal is a declared error, and one the model agrees with
I9COVERAGE: the sequence actually pulled, lapsed, ran a budget out and paid a tip at the cap
I10the price guard: every landed pull bought at least the minimum the feed implied, and a pool that cannot meet it is refused
I11light() 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 brokenSeedsCaught by
B1the schedule never re-arms (nextAt is left where it was)3/3I3
B2a lapsed wick catches up (nextAt += period even when that is in the past)3/3I3
B3the budget is never spent down3/3I3
B4the tip is not capped3/3I6
B13the tip has no floor, so a small draw pays a caller less than their gas3/3I6
B5the shares are sent to whoever pulled, not to the owner3/3I7
B6the minimum is thrown away, so any pool is acceptable3/3I10
B7anyone may snuff anyone's wick3/3I3
B8a spent wick is not closed3/3I3
B9a budget that is not a whole number of draws is accepted3/3I11
B10a wick may be pulled the moment it is lit and again immediately2/3I8
B11the price is read with the wrong scale (a factor of a million)3/3I10
B12the tip is taken on top of the draw rather than out of it3/3I8

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.

StockPrice$5$25$100$1,000Pools
SGOV iShares 0-3 Month Treasury Bond$101.11-58 bps-58 bps-58 bps-58 bps2
AMD AMD$559.42-51 bps-51 bps-50 bps-43 bps2
GME GameStop$22.55-19 bps-18 bps-18 bps-17 bps3
PLTR Palantir Technologies$177.60-16 bps-16 bps-15 bps-13 bps1
NVDA NVIDIA$222.45-13 bps-13 bps-13 bps-13 bps2
ASML ASML Holding NV$1,676.63-12 bps-10 bps-6 bps+51 bps2
MU Micron Technology$1,014.43-4 bps-4 bps-4 bps-3 bps1
AAPL Apple$335.380 bps0 bps0 bps+1 bps3
MSFT Microsoft$495.82+11 bps+11 bps+11 bps+12 bps1
GOOGL Alphabet Class A$350.47+23 bps+23 bps+23 bps+23 bps2
SPY SPDR S&P 500 ETF Trust$761.55+26 bps+26 bps+26 bps+27 bps2
TSM Taiwan Semiconductor Manufacturing$434.26+31 bps+32 bps+32 bps+37 bps1
CRCL Circle Internet Group$91.76+37 bps+37 bps+37 bps+38 bps1
USAR USA Rare Earth$15.61+37 bps+37 bps+37 bps+45 bps1
TSLA Tesla$363.80+45 bps+45 bps+47 bps+58 bps3
AMZN Amazon$253.86+48 bps+48 bps+48 bps+49 bps1
SNDK Sandisk Corporation$1,792.32+51 bps+52 bps+53 bps+68 bps1
SLV iShares Silver Trust$59.85+54 bps+73 bps+75 bps+76 bps3
META Meta Platforms$666.76+85 bps+85 bps+85 bps+87 bps2
INTC Intel$109.05+95 bps+95 bps+96 bps+102 bps1
BABA Alibaba$113.77+110 bps+110 bps+111 bps+117 bps2
DELL Dell$570.72+143 bps+143 bps+143 bps+147 bps1

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.

StockWhy not
AMCits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name AMC
COSTthe Morpho oracle for this token names no price feed
DJTits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name DJT
GLDits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name GLD
HIMSthe Morpho oracle for this token names no price feed
IBMits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name IBM
LLYits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name LLY
LULUits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name LULU
MRNAits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name MRNA
NFLXthe Morpho oracle for this token names no price feed
QQQthe Morpho oracle for this token names no price feed
RBLXits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name RBLX
RDDTthe Morpho oracle for this token names no price feed
RIVNits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name RIVN
SPCXthe Morpho oracle for this token names no price feed
TTWOits only on-chain price is "Uniswap V3 Pool Price in USD", which does not name TTWO
USOthe Morpho oracle for this token names no price feed
COINno v3 pool that Uniswap's factory confirms
EWYno v3 pool that Uniswap's factory confirms
NBISno v3 pool that Uniswap's factory confirms
ORCLno v3 pool that Uniswap's factory confirms
RKLBno v3 pool that Uniswap's factory confirms
MSTRcheapest $25 buy costs 410 bps over the feed, above the 300 bps bar

What it costs

TransactionGasAt today's price
Light a wick (approve + light)231,2924.00c
Pull a due wick266,4174.61c
Snuff a wick32,7990.57c
Deploy Wick (once, by the first person)1,927,44033.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.