What Weir is
Weir is two contracts on Robinhood Chain. WeirFactory opens one WeirVault per Uniswap v3 stock/USDG pool. A vault holds a single concentrated liquidity position in its pool and issues ERC-20 shares against it (weirNVDA, weirCRCL…). Trades through the position's range pay the pool's fee; the vault collects the fees and reinvests them, so each share holds more of both tokens over time.
There is no owner, no admin key, no protocol fee, no upgrade path and no pause. The factory is 21,310 bytes, each vault 16,772 bytes. Source: WeirVault.sol, WeirFactory.sol, UniMath.sol.
Using a vault
zapIn(tokenIn, amountIn, minShares, to, deadline)
Pays in one token. The vault swaps the part it needs into the other token through its own pool, deposits both in the vault's current proportion and returns whatever it could not use in the same transaction. This is what the app's Deposit button sends, with USDG.
deposit(max0, max1, minShares, to, deadline)
Pays in both tokens, in the vault's proportion, taking at most max0 and max1. Nothing is swapped.
withdraw(shares, min0, min1, to, deadline)
Burns shares for their exact slice of the position and of the uninvested balance, both rounded down. Never refused on price.
zapOut(shares, tokenOut, minOut, to, deadline)
Withdraws and swaps the other side into tokenOut through the pool: one token back.
tend()
Anyone may call it. It collects the fees; if the price is inside the range they are reinvested, and if it has left the range the whole position is taken out, rebalanced by one swap and put back centred on the price. There is no reward for calling it, and no need for one: every zap does the same first.
The rules
- Range. A fixed number of ticks either side of the price, set when the vault is opened: about ±10.7% for every stock pool (1,000 ticks rounded to the pool's spacing). The range is re-centred only when the price has left it.
- Price guard. Every action that trades at the pool's price — zaps, deposits,
tend— first compares the pool's tick with its own 300-second time-weighted average and refuses (PriceMoved) if they are more than 200 ticks (about 2%) apart. Every swap the vault makes is also limited to that band. A price pushed within one block is therefore refused, and a price held off for minutes has to be paid for against every arbitrageur on the chain. - Shares. The first deposit mints shares equal to the liquidity it adds, and locks a million of them at
0x…dEaDforever, so the first depositor cannot inflate the share price to round the next one down to nothing. After that, shares are the smallest proportion of the vault's holdings that the deposit covers, rounded down; what is taken is rounded up. - Fees. Collected into the vault on every deposit, withdrawal, zap and tend, before any shares are priced, so a newcomer never buys fees that were already earned.
Addresses
The factory deploys through the deterministic CREATE2 deployer that is already on Robinhood Chain (0x4e59b44847b379578588920ca78fbf26c0b4956c) with salt keccak256("weir.v1"). Its address is a hash of its code, and each vault's address is a hash of the factory, the pool and the width — so every address is fixed before anything is deployed, and the first person to deposit into a vault opens it, factory included, in their first transaction.
WeirFactory: 0x868CAd2f2384e5ED691BBA6682387c8FEb1FAA68
Init code hash: 0x3d55d54661d2a336a875f415187e0df44a1228b9cf4b61088a83210736d4b225
Runtime code hash: 0x760067cf83bfb0b226c46a27069ec4bb4edac481c41070b4d5a60e8fe4d8d495
The app refuses to run if the code at that address hashes to anything else, and checks every vault address it uses against the factory's own record.
Tests
Every property below is a Solidity function run against live Robinhood Chain inside one eth_call: the harness deploys the factory from its init code, opens vaults on the real NVDA, SPCX and CRCL pools, trades through them and checks balances it computes itself. Nothing is sent and nothing stays deployed. 25 properties, 149 assertions, at block 70852680. Source: test/WeirTest.sol.
| # | Property | Assertions | Result |
|---|---|---|---|
| 1 | factory | 13 | pass |
| 2 | firstZap | 10 | pass |
| 3 | depositInKindFair | 3 | pass |
| 4 | depositTakesWhatItSays | 7 | pass |
| 5 | withdrawInKind | 6 | pass |
| 6 | roundTrip | 3 | pass |
| 7 | feesAccrue | 6 | pass |
| 8 | recentre | 6 | pass |
| 9 | twapGuard | 9 | pass |
| 10 | minimums | 9 | pass |
| 11 | deadline | 8 | pass |
| 12 | callbacks | 5 | pass |
| 13 | othersShares | 6 | pass |
| 14 | donation | 3 | pass |
| 15 | inflation | 5 | pass |
| 16 | tickMath | 15 | pass |
| 17 | sequence | 6 | pass |
| 18 | usdgIsToken1 | 3 | pass |
| 19 | zapWithStock | 2 | pass |
| 20 | tooSmall | 8 | pass |
| 21 | secondZapFair | 2 | pass |
| 22 | zapOutStock | 3 | pass |
| 23 | recentreWhileZapping | 2 | pass |
| 24 | leaverTakesOnlyOwn | 6 | pass |
| 25 | feesBeforeNewcomer | 3 | pass |
Deliberate bugs
A suite that passes is only worth something if it fails when the code is wrong. Each row below is a deliberate bug planted in a copy of the contracts; the suite must go red on it, and on the property named for it. 17/17 caught.
| Bug | What it breaks | Result |
|---|---|---|
twap-off | the price guard never refuses | caught by twapGuard |
mint-callback-open | anyone can make the vault pull from a wallet that approved it | caught by callbacks |
swap-callback-open | anyone can make the vault pay out its idle balance | caught by callbacks |
no-deadline | a transaction that sat in a queue still executes | caught by deadline |
no-dead-shares | the first depositor can inflate the share price | caught by firstZap |
deposit-pays-floor | a depositor pays less than their share of token0 | caught by depositTakesWhatItSays |
withdraw-idle-ceil | a leaver takes one unit more than their share | caught by withdrawInKind |
burn-too-much | a leaver takes 0.1% of everyone else's liquidity | caught by withdrawInKind |
zapout-takes-idle | zapping out also takes half the others' uninvested token0 | caught by leaverTakesOnlyOwn |
never-recentre | tend never re-centres a vault that has left its range | caught by recentre |
zap-no-tend | a zap deposits into a range the price has left | caught by recentreWhileZapping |
no-compound | collected fees are never put back to work | caught by feesAccrue |
deposit-skips-collect | a newcomer buys fees earned before they arrived | caught by feesBeforeNewcomer |
range-offset | the range is not centred on the price | caught by firstZap |
zap-swaps-all | a zap swaps everything and hands most of it back unused | caught by firstZap |
factory-any-pool | the factory opens a vault on a contract that only looks like a pool | caught by factory |
shares-round-up | a depositor gets one share more than they paid for | caught by depositTakesWhatItSays |
How the fee rates are measured
tools/scan-vaults.mjs reads every Uniswap Swap event in each stock's pool over the last day. It replays them against a $10,000 range centred on the first swap's price: a swap inside the range pays it the pool fee times l / (L + l), where L is the pool's other active liquidity (the event reports it) and l is the range's own. A swap outside re-centres the range, charged half the pool fee on the whole position. The total is annualised. It is fees only: the loss a liquidity position takes when the price moves is not in it, and is the main risk of using Weir.
Risks
- Price moves. A position in a range sells the stock as it rises and buys it as it falls. After a large move in either direction it is worth less than the two tokens would have been, and the fees may not make up the difference.
- Out of range. Earns nothing until tended. Re-centring costs one swap's pool fee on part of the position.
- The stock tokens. Robinhood can pause any stock token, block an address or burn from it. A vault holding that stock would be affected like any other holder.
- Not audited. The code is small and tested against the real pools; nobody independent has reviewed it.