Highwater

Docs

Highwater trades Stock Tokens on Robinhood Chain with trailing stops: a stop-loss that follows every new Chainlink high and never moves down. One router contract trades USDG and the 27 listed Stock Tokens through their Uniswap pools, and refuses any fill that lands too far from the Chainlink price of the stock. Next to trailing stops it runs market swaps, limit orders with brackets, stock to stock trades in one transaction and EIP-2612 permits instead of approvals.

Overview

Every Stock Token on Robinhood Chain has two prices: the price its Uniswap pool will pay right now, and the price Chainlink reports from the stock market. Most of the time they agree. When a pool is thin, has not been arbitraged since the last move, or is being sandwiched, they do not, and a normal swap fills at the pool's price anyway.

Highwater reads Chainlink inside the contract and turns it into a hard minimum for every leg of every trade. You keep the pool's liquidity and the pool's price when it is fair; you get a revert when it is not.

Trailing stops

A trailing stop is a sell order whose stop price sits a fixed distance under the highest Chainlink price seen since you placed it. You choose that distance, the trail, between 0.5% and 30% (the app offers 2%, 5%, 10% and 15%).

Placing. placeTrailing(token, amount, trailBps, expiry) moves the Stock Tokens into the contract and records the current Chainlink price as the peak. The stop starts at peak × (1 − trail). placeTrailingWithPermit does the same with a permit signature instead of an approval. Trailing stops are sell orders only: they protect a stock you already hold.

Raising. raise(id) reads Chainlink and, if the price is above the stored peak, stores the new peak and lifts the stop to the new peak × (1 − trail). It reverts if there is no new high, so the stop can never go down. Anyone may call it; it pays nothing and costs only gas. raisable(from, count) lists the open trailing stops whose stop can be lifted right now.

Selling. When Chainlink is at or under the stop, the order is due and fill(id) sells it on the pool, inside your band of Chainlink, exactly like the stop side of a bracket. The keeper earns $0.05 from the proceeds.

Example. You place 2.5 NVDA with a 5% trail at $200: the stop is $190. NVDA prints $214.60, the stop goes to $203.87. NVDA prints $226.30, the stop goes to $214.99. A dip to $219 changes nothing. NVDA falls to $214.85: the order sells for about $537 USDG.

Between checks. The keeper looks every few minutes. A high that comes and goes between two checks is not recorded, so the stop follows the highs the keeper sees, not every tick. Over a weekend the feed is paused and the stop stays where it is.

Cancel. cancel(id) returns the Stock Tokens to you at any time, like any other order.

A market swap, step by step

Calling swap(tokenIn, tokenOut, amountIn, minOut, slipBps, to):

If any check fails, the whole transaction reverts. The router never keeps a balance between trades.

The Chainlink floor

For each pool leg the router computes the amount Chainlink says you should get, then takes off your band. Buying NVDA with 1,000 USDG at a Chainlink price of $217.07 and a 0.5% band:

The same check runs on sells (minimum USDG out from the Chainlink value of what you sell) and on each leg of a stock to stock trade. quote(tokenIn, tokenOut, amountIn, slipBps) returns the fair amount and the floor, so you can see both before you sign.

Choosing a band

The band is how far from Chainlink you accept a fill. You pick it per trade: 0.3%, 0.5%, 1% or 1.5%. It can never be looser than the router's cap, 1.5% at launch.

The pool fee is part of what the pool delivers, so a 1% fee pool cannot fill inside a 0.3% band. The app shows the live gap between each pool and Chainlink, which is the quickest way to pick.

Stock to stock

Rotating from TSLA to NVDA on a usual swap page takes two swaps through USDG and two approvals. Highwater does it in one call: the TSLA leg is sold with a Chainlink floor, 0.1% is taken from the USDG in the middle, and the NVDA leg is bought with its own floor. The floor returned by quote counts the band twice, once per leg.

Permit instead of approve

USDG and the Stock Tokens implement EIP-2612. swapWithPermit and placeOrderWithPermit take the allowance as a signature for exactly the amount of the trade, so nothing stays approved after it.

Orders and brackets

placeOrder(token, buy, amount, limit, stop, expiry) creates an order on one Stock Token with up to two triggers. Either price may be 0, not both:

The details of each side:

The fill. fill(id) checks Chainlink has reached the limit, then trades on the pool with the usual Chainlink floor and one more check: the price you actually get must be no worse than your limit. A buy of 500 USDG with a $195 limit must deliver at least (500 − fees) / 195 NVDA; a sell must deliver at least the amount times $195. If the pool cannot meet that yet, the fill reverts and the order keeps waiting.

Cancel and expiry. cancel(id) returns everything in the order to you, at any time, from the owner of the order only. Once an order passes its expiry, any fill call closes it and returns the funds instead of trading.

due(from, count) lists the open orders that can be filled or closed now. getOrder(id) and ordersCount() read the book.

The keeper

A Cloudflare worker checks the book every few minutes. First it calls raise on every trailing stop with a new high, then fill on every due order. It earns $0.05 per fill from the order, which pays its gas; it refuels its own ETH from that USDG. Anyone else may call fill too and earn the same fee, so orders do not depend on Highwater's keeper staying up.

Fees

Market swap0.1% of the USDG side, plus the pool fee and gas
Stock to stock0.1% once, on the USDG in the middle, plus both pool fees
Limit order0.1% of the USDG side plus $0.05 to whoever fills it
Cancel, expiryfree (gas only for a cancel)
Hard capsthe fee can never exceed 0.5%, the fill fee $0.50

Example: buying $1,000 of NVDA costs $1 to Highwater and about $0.50 to the 0.05% pool. A $500 limit buy costs $0.50 plus $0.05.

Pools and routes

Each Stock Token trades through its deepest Uniswap pool against USDG. COIN, MSTR and SPY route through WETH, because that is where their liquidity sits; each of their legs is still checked against Chainlink as a whole.

AAPLUSDG / AAPL, 0.05% pool
NVDAUSDG / NVDA, 0.05% pool
TSLAUSDG / TSLA, 0.3% pool
MSFTUSDG / MSFT, 0.3% pool
AMZNUSDG / AMZN, 0.3% pool
METAUSDG / META, 0.3% pool
GOOGLUSDG / GOOGL, 0.05% pool
COINUSDG / WETH 0.01%, then WETH / COIN 0.3%
AMDUSDG / AMD, 0.3% pool
TSMUSDG / TSM, 1% pool
MUUSDG / MU, 0.3% pool
INTCUSDG / INTC, 0.3% pool
PLTRUSDG / PLTR, 0.3% pool
DELLUSDG / DELL, 1% pool
SNDKUSDG / SNDK, 1% pool
MSTRUSDG / WETH 0.01%, then WETH / MSTR 1%
CRCLUSDG / CRCL, 0.3% pool
BABAUSDG / BABA, 0.3% pool
GMEUSDG / GME, 1% pool
SPCXUSDG / SPCX, 0.05% pool
USARUSDG / USAR, 0.3% pool
SPYUSDG / WETH 0.01%, then WETH / SPY 0.05%
QQQUSDG / QQQ, 0.05% pool
GLDUSDG / GLD, 0.3% pool
SLVUSDG / SLV, 0.3% pool
USOUSDG / USO, 0.3% pool
SGOVUSDG / SGOV, 0.3% pool

Market hours and weekends

Chainlink stock feeds update while the market is open and pause between sessions. The router accepts a Chainlink answer up to 3.5 days old, so it keeps working over weekends using the last close. Pools keep trading 24/7, so a pool that runs away from Friday's close is refused until Chainlink catches up on Monday. Limit orders likewise fill on Chainlink, not on weekend pool moves.

The app

For bots and integrators

Everything the app does is plain contract calls. A buy with viem:

const [fair, floor] = await client.readContract({ address: ROUTER, abi, functionName: "quote", args: [USDG, NVDA, 1_000_000000n, 50] });
await wallet.writeContract({ address: ROUTER, abi, functionName: "swap", args: [USDG, NVDA, 1_000_000000n, floor, 50, me] });

// sell 2.5 NVDA: target $230, stop $205, valid for 7 days
await wallet.writeContract({ address: ROUTER, abi, functionName: "placeOrder", args: [NVDA, false, 2_500000000000000000n, 23000000000n, 20500000000n, now + 7n * 86400n] });
// trailing stop: sell 2.5 NVDA if it falls 5% from its highest price
await wallet.writeContract({ address: ROUTER, abi, functionName: "placeTrailing", args: [NVDA, 2_500000000000000000n, 500, 0] });

The site serves /api/prices (Chainlink and 24h change), /api/pegs (pool price, address, liquidity and volume per stock) and /api/orders?a=0x… (the open book and one trader's orders).

Events

Owner limits

The owner of the router can list Stock Tokens and set their routes, change the price adapter, set the band cap (up to 5%), the fee (up to 0.5%), the fill fee (up to $0.50) and the treasury. There is no function that moves a trader's tokens or an order's funds anywhere except through a trade that passes the checks, or back to the trader.

Risks

FAQ

Does Highwater have its own liquidity? No. It routes through the existing Uniswap pools and adds the checks.

Why did my swap revert? The pool was outside your band of Chainlink, or the total was under minOut. Try a wider band, a smaller size, or wait for the pool to come back in line.

Can a limit order fill at a worse price? No. The contract checks the fill against your limit and reverts otherwise.

What happens to my order if the keeper stops? Nothing is lost. Anyone can fill it, and you can cancel at any time.

Token

The Highwater token launches on pons. Its contract address appears on the home page and here once it is live. Always check the address on this site before trading.

Contracts

Highwater routerOpening soon
TokenLaunching soon on pons
Chainlink adapter0xe9bBcd46F9B0Df04B418e0207876984F9f8336cb
USDG0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
Uniswap SwapRouter020xcaf681a66d020601342297493863e78c959e5cb2
Uniswap QuoterV20x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7