Start here
How vaults work
One Stock Token, one Uniswap v3 position against USDG, and a share price that rises as fees accrue.
A share is a claim on the vault
Each vault is an ERC-4626 contract for one Stock Token. A share is not a receipt for a fixed number of tokens. It is a claim on everything the vault holds, divided by the share supply. As swaps pay fees, total assets rise and each share is worth more of the Stock Token.
| Component | What it is | How it is priced |
|---|---|---|
| Idle balances | Stock Token and USDG waiting to be deployed. | Face value, expressed in the Stock Token. |
| Position | A single Uniswap v3 position, both legs, inside the band. | Pool TWAP over at least 10 minutes. |
| Uncollected fees | Fees the position has earned and that remain unharvested. | Chainlink, with USDG at $1. |
Deposit and mint
The vault has two entry functions. deposit takes an asset amount. mint takes a share amount. Both use the current share price, both require an Ok oracle status, and both stop when the vault is paused.
The asset is always the Stock Token. A USDG holder goes through UptickRouter.depositUsdg, which swaps USDG to the Stock Token at the Chainlink-implied value less a slippage bound, then calls deposit. The router refuses the swap when the oracle status is not Ok.
After a deposit, instantDeploy decides whether the vault acts immediately. When it is on, the vault balances its idle inventory toward the ratio the range needs, swapping stock to USDG where necessary, and adds both legs to the position. When it is off, the deposit sits idle until the next rebalance. The deposit cap is a ceiling on total assets: maxDeposit returns the remaining room, not a per-ticket limit.
| Path | What happens | When it is available |
|---|---|---|
| Stock Token, instantDeploy | Deposit, balance idle inventory, add both legs to the position, mint shares. | Oracle status Ok, not paused, and room under depositCap. |
| Stock Token, no instantDeploy | Hold the Stock Token idle, mint shares, deploy at the next rebalance. | Oracle status Ok, not paused, and room under depositCap. |
| USDG via router | Swap USDG to the Stock Token at the Chainlink value less slippage, then deposit. | Router requires oracle status Ok. |
| Reduced replay state | Same contract paths. The app and prepare_deposit cap a single ticket at $25,000. | The weaker replay half is between -20% a year and break-even. |
The band and the recentre
The position sits inside a plus or minus 5% band around the Chainlink price, expressed as rangeBps 500 and snapped to the pool's tick spacing. The band is the product: it earns fees while the stock trades inside it, and it defines when the position has to move.
A recentre collects fees, withdraws both legs, and redeploys around the new Chainlink price. The keeper runs a 60 second loop and treats three cases differently.
| Case | What the keeper does |
|---|---|
| out_of_range | Ticks outside the band for 30 minutes, at least 2 hours after the last rebalance, and only during an NYSE trading day. |
| Opening recentre | Rebalances at 02:00 UTC after each NYSE session, before the next session opens. |
| Weekend or holiday | Never triggers an out-of-range recentre, even if the tick leaves the band. |
| 26 hours without a rebalance | Anyone may call rebalance(). The permissionless path is the backstop. |
Exit paths
Exits are not symmetrical. The standard ERC-4626 paths pay the Stock Token and need a healthy oracle. The in-kind path pays both tokens and does not.
| Path | What you receive | Oracle gate |
|---|---|---|
| withdraw | A specific asset amount, in the Stock Token. | Required. |
| redeem | The asset value of a specific share amount, in the Stock Token. | Required. |
| redeemInKind | Your proportional share of both the Stock Token and USDG, with no swap. | Not required. |
| maxWithdraw | The largest withdrawal the vault will allow for an owner. | None. It is a view of share value at the current mark. |
An in-kind exit can hand you more of one token than you expect because the vault holds both legs of the position and idle balances in both. It does not route through a swap, so it does not depend on pool liquidity at the moment you exit. When the Stock Token holds a transfer, the vault records the Stock Token leg against the receiver and claimStockOwed pays it out, so the exit always completes.
The oracle gate
Every priced path reads the same gate. If any check fails, the path refuses rather than pricing on a bad market. The read has five ways to fail, and each one maps to a status code.
| Check | Rule |
|---|---|
| FeedInvalid | The feed reverts, the answer is zero or negative, the timestamp is zero or in the future, or answeredInRound is behind roundId. |
| FeedStale | block.timestamp - updatedAt is greater than maxStaleness. |
| TwapUnavailable | pool.observe reverts because history is shorter than twapWindow. |
| TwapDeviates | The TWAP differs from Chainlink by more than maxDeviationBps. |
| SpotDeviates | The spot price differs from Chainlink by more than maxDeviationBps. |
Fees, shares and the treasury cut
The performance fee is 10% of uncollected fees. A harvest collects those fees, values them, and mints the treasury its cut as shares. The mint is priced against assets net of the cut, so the share price is the same one second before and one second after a harvest.
Shares use 24 decimals, six more than the 18-decimal Stock Token. The extra precision is an ERC-4626 inflation guard. It does not change the value of a share, only the number of decimal places used to express it.
Parameters
| Parameter | Value | What it controls |
|---|---|---|
| maxStaleness | 90,000 seconds | How old a Chainlink answer may be. Allowed range 1 hour to 7 days. |
| maxDeviationBps | 100 bps | How far the TWAP and the spot price may sit from Chainlink. Allowed range 10 to 500 bps. |
| twapWindow | 600 seconds | The TWAP observation window. Minimum 600 seconds. |
| rangeBps | 500 bps | The half-width of the band around Chainlink. |
| perfFeeBps | 1,000 bps | The treasury cut on uncollected fees. |
| Share decimals | 24 | The ERC-4626 inflation guard. |
| minRebalanceInterval | 6 hours | The shortest gap between keeper rebalances. |
| Permissionless rebalance | 26 hours | When anyone may call rebalance(). |
| Deposit ceiling | Up to $100,000 | The vault-sized cap; the largest is $100,000. |
Read the oracle gate
The app already exports the vault list and ABI, so the gate is one contract read away. This is the same call the deposit and withdraw paths make.
import { createPublicClient, http } from "viem";
import { robinhoodChain } from "@/lib/chain";
import { vaultAbi } from "@/lib/abis";
import { getVault } from "@/lib/vaults";
const client = createPublicClient({
chain: robinhoodChain,
transport: http(),
});
const vault = getVault("nvda");
if (!vault?.vault) throw new Error("Set NEXT_PUBLIC_VAULTS to the vault address map");
const [status, price, updatedAt, sqrtOracle, sqrtTwap, sqrtSpot] =
await client.readContract({
address: vault.vault,
abi: vaultAbi,
functionName: "health",
});A non-zero status means the gate is refusing. The price and timestamps are returned with the status so a caller can show why, rather than reporting a generic failure.
Sources
contracts/README.mdShare definition, deposit, rebalance, redeem, fees and the oracle gate.
keeper/README.mdThe 60 second loop, triggers, guards and event lines.
web/public/llms.txtCanonical gate states, caps and current values.