Uptick

Connect a wallet

Uptick reads your balance and asks you to sign each transaction. It never holds your keys.

Docs ยท How vaults work

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.

ComponentWhat it isHow it is priced
Idle balancesStock Token and USDG waiting to be deployed.Face value, expressed in the Stock Token.
PositionA single Uniswap v3 position, both legs, inside the band.Pool TWAP over at least 10 minutes.
Uncollected feesFees the position has earned and that remain unharvested.Chainlink, with USDG at $1.
totalAssets() sums these components before dividing by totalSupply().

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.

PathWhat happensWhen it is available
Stock Token, instantDeployDeposit, balance idle inventory, add both legs to the position, mint shares.Oracle status Ok, not paused, and room under depositCap.
Stock Token, no instantDeployHold the Stock Token idle, mint shares, deploy at the next rebalance.Oracle status Ok, not paused, and room under depositCap.
USDG via routerSwap USDG to the Stock Token at the Chainlink value less slippage, then deposit.Router requires oracle status Ok.
Reduced replay stateSame 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 keeper's rebalance is the path that turns idle balances into a position outside a deposit transaction.

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.

CaseWhat the keeper does
out_of_rangeTicks outside the band for 30 minutes, at least 2 hours after the last rebalance, and only during an NYSE trading day.
Opening recentreRebalances at 02:00 UTC after each NYSE session, before the next session opens.
Weekend or holidayNever triggers an out-of-range recentre, even if the tick leaves the band.
26 hours without a rebalanceAnyone may call rebalance(). The permissionless path is the backstop.
The keeper simulates the transaction before sending it and logs the result as a JSON line.

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.

PathWhat you receiveOracle gate
withdrawA specific asset amount, in the Stock Token.Required.
redeemThe asset value of a specific share amount, in the Stock Token.Required.
redeemInKindYour proportional share of both the Stock Token and USDG, with no swap.Not required.
maxWithdrawThe largest withdrawal the vault will allow for an owner.None. It is a view of share value at the current mark.
redeemInKind settles when Chainlink is stale or the vault is paused. If the Stock Token transfer reverts, the stock leg is credited and claimStockOwed pays it later.

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.

CheckRule
FeedInvalidThe feed reverts, the answer is zero or negative, the timestamp is zero or in the future, or answeredInRound is behind roundId.
FeedStaleblock.timestamp - updatedAt is greater than maxStaleness.
TwapUnavailablepool.observe reverts because history is shorter than twapWindow.
TwapDeviatesThe TWAP differs from Chainlink by more than maxDeviationBps.
SpotDeviatesThe spot price differs from Chainlink by more than maxDeviationBps.
Defaults are maxStaleness 90,000 seconds, maxDeviationBps 100 and twapWindow 600 seconds. The first failing check wins.

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

ParameterValueWhat it controls
maxStaleness90,000 secondsHow old a Chainlink answer may be. Allowed range 1 hour to 7 days.
maxDeviationBps100 bpsHow far the TWAP and the spot price may sit from Chainlink. Allowed range 10 to 500 bps.
twapWindow600 secondsThe TWAP observation window. Minimum 600 seconds.
rangeBps500 bpsThe half-width of the band around Chainlink.
perfFeeBps1,000 bpsThe treasury cut on uncollected fees.
Share decimals24The ERC-4626 inflation guard.
minRebalanceInterval6 hoursThe shortest gap between keeper rebalances.
Permissionless rebalance26 hoursWhen anyone may call rebalance().
Deposit ceilingUp to $100,000The vault-sized cap; the largest is $100,000.
Defaults per vault. Per-token deposit caps and gate states live on each vault page.

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.

TypeScript, viem
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.