Integration
| Chain | Robinhood Chain mainnet, 4663 (RPC https://rpc.mainnet.chain.robinhood.com) |
| Explorer | robinhoodchain.blockscout.com |
| A launch's curve and pool | quoted in the stock; the pool is Uniswap v4 on the shared hook, fee 0.20%, tick spacing 60 |
| The treasury's stock leg | the listing's stock/USDG Uniswap V3 pool, with a 600-second TWAP |
| Stock oracle | Chainlink, RHxxx / USD ÷ the USDG feed, with the rules of the three prices |
| Block time | about 0.1 s; every window is timed in seconds |
Launching a token
A launch is one Request plus terms, the hash predict(request) returned. Register the creator's choices first,
quote, then launch in the same terms.
The request
| field | what it is |
|---|---|
name, symbol | the token's |
stock | a listed stock; for any other, predict reverts without a reason |
creator | receives creatorBps of the collected tax, forever, and may veto a payout proposal. An EOA or a Safe |
taxBps | 100 to 1500 |
creatorBps | 0 to 5000, of the collected tax (maxCreatorBps). The launch form shows a base tax plus a creator tax at most equal to it, and sends taxBps = base + creator and creatorBps = creator ÷ total |
tp1Bps, tp2Bps, dipBps, stopBps, lotBps | the rule numbers; meaning and bounds depend on the strategy |
bandBpsPerHour | the staleness band, 0 to bandCeiling(stock) |
nonce | separates two launches of the same symbol by the same creator; part of the salt |
maxFee | the most the launcher will pay as launch fee |
expectedOpenPriceE18 | the listing's opening price the launcher saw |
Before the quote: the creator's registrations
All four are keyed by keccak256(abi.encode(symbol, msg.sender, nonce)), the salt the factory derives from
(symbol, creator, nonce), so a registration from another address never reaches this launch.
curveDeployer.setCurveConfig(symbol, nonce, 7931, snipeSeconds) // window 0..180 s; saleBps must be DEFAULT_SALE_BPS
curveDeployer.setOpeningTaxExemptions(symbol, nonce, recipients) // up to 40; the creator is exempt without this
treasuryDeployer.setStrategyKind(symbol, nonce, kind) // 1..6; kind 0 needs no call
treasuryDeployer.setEngineConfig(symbol, nonce, kind, config) // kinds 2, 3 and 6 only
A registration can be changed until the launch and not deleted. Changing one after predict makes the launch
revert Restated; registering the quoted values again repairs the quote.
Quote and launch
(token, treasury, terms) = factory.predict(request)
id = factory.launch{value: launchFeeAmount}(request, terms)
id = factory.launchWithMetadata{value: launchFeeAmount}(request, terms, metadata)
launchFeeCurrency is Native and launchFeeAmount 0.0005 ETH on the release defaults; read getDefaults().
publicLaunch() must be true, or the caller must be an authorised launcher (NotOpen otherwise). predict and
launch both run the deployers' validation, so a request that would fail at launch is refused at predict with
a named error: BadCurveConfig, BadEngineConfig, StopInsideExecutionFriction, Unseedable. The exceptions are
Cycle (kind 5) and Lots with reserve (kind 6), whose rung floor (tp1Bps and dipBps at least 2 × (maxSlippageBps + pool fee)) is checked
only by the constructor (for kind 6, so is reserveBps over 50%); a quote under it gets terms and the launch reverts TreasuryDeployFailed. Apply that
floor before quoting.
Show the creator the listing check's verdict before they sign: a raise the stock's pool cannot deliver can never graduate, and nothing on chain refuses such a launch.
One ETH payment: launch and first buy
launchNativeRouter.launchAndBuy{value: launchFeeAmount + b.amountIn}(request, terms, metadata, b, path)
b is (amountIn, minStockReceived, minFinalOut, deadline, allowPartialFill); path is the V3 route from WETH
to the stock (commonly WETH → USDG → stock). The router pays the fee to the factory, wraps the rest, routes it to
the stock and buys on the new curve for request.creator, so the creator's opening-premium exemption applies and
the base tax does too. Everything reverts together on a stale quote, a bad route, a failed buy or a failed fee
transfer. The owner must have authorised the router with setLauncher(router, true). Do not use the v1
HedgeFunLaunchRouter for a v2 launch: its first buy goes straight to v4.
Metadata and the icon
launchWithMetadata and launchAndBuy write the token's first entry in the launch transaction, so the token is never
live with an empty card. The entry is (logo, description, (twitter, telegram, discord, website, farcaster), extraURI):
each link at most 256 bytes and the description at most 1,024 bytes (TooLong otherwise).
The site shows a logo only when it is an icon the site stores, https://hedgefun.trade/media/icons/<sha256>.png.
A logo pointing anywhere else stays on chain and is not displayed: a third-party host would see every visitor and
could swap the image after the launch was reviewed. Social links are shown only as https links on the platform's
own host (x.com or twitter.com, t.me, discord.gg or discord.com, farcaster.xyz or warpcast.com).
There are two ways to get a stored icon:
-
By hand: upload on hedgefun.trade/icons and copy the URL. No wallet is connected.
-
From a script:
POST /api/iconswith the PNG, signed by a wallet that holds at least the launch fee in ETH on the chain. The signed message isHedgefun icon uploadHost: hedgefun.tradeSHA-256: <lowercase hex SHA-256 of the exact PNG bytes>Issued: <Unix seconds, within 300 s of the server's clock>
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { privateKeyToAccount } from 'viem/accounts';
const png = await readFile('icon.png');
const sha256 = createHash('sha256').update(png).digest('hex');
const issued = Math.floor(Date.now() / 1000);
const creator = privateKeyToAccount(process.env.CREATOR_KEY as `0x${string}`);
const signature = await creator.signMessage({
message: `Hedgefun icon upload\nHost: hedgefun.trade\nSHA-256: ${sha256}\nIssued: ${issued}`,
});
const response = await fetch('https://hedgefun.trade/api/icons', {
method: 'POST',
headers: { 'content-type': 'image/png', 'x-icon-issued': String(issued), 'x-icon-signature': signature },
body: png,
});
const { url, error } = await response.json(); // url: https://hedgefun.trade/media/icons/<sha256>.png
The PNG must be 8-bit RGB or RGBA, not interlaced, at most 512 x 512 pixels and 512 KiB, with no text, colour
profile or animation chunks. sharp(input).resize(512, 512, { fit: 'inside', withoutEnlargement: true }) .ensureAlpha().png() produces one. Only EOA signatures are accepted. Uploading the same image twice returns the same URL.
After the launch the creator (the token's deployer) can replace the whole entry with
token.setMetadata(logo, description, socials, extraURI), or appoint one other address to do it with
setEditor(editor) (address(0) dismisses it), which suits a bot that launches for a creator. lock() freezes the
entry for ever; it is refused while an editor is appointed, so dismiss the editor, check the page, then lock.
The token page has an editor for the creator that does the same through a wallet.
What to show from live values
The raise, the LP share and the treasury share come from the listing's openPriceE18, DEFAULT_SALE_BPS() on
the curve deployer, the supply, lpBps(stock) on the treasury deployer and the oracle price. The target is fixed
in stock units at launch; a USD figure is an estimate.
Buying and selling
Use HedgeFunV2TradeRouter for any payment asset, HedgeFunV2NativeRouter for ETH, or call the curve and the
pool directly when you hold the stock.
The trade router
buy(params, path) and sell(params, path) take one TradeParams and a Hop[]; buyFor(params, path, to)
delivers to another address.
| field | meaning |
|---|---|
id | the launch id on this factory |
asset | buy: the payment ERC20; sell: the desired output ERC20 |
amountIn | buy: payment in the asset's raw units; sell: strategy tokens |
minStockReceived | buy: the least stock the payment route must produce, including stock later refunded; ignored on sells |
minFinalOut | the least final output after every swap and tax, never scaled down on a partial fill |
deadline | last permitted timestamp |
expectedStage | 0 Active or 2 Graduated, checked before funds move |
allowPartialFill | accept a stock refund on a buy, or a token refund on a sell |
Hop.pool, Hop.tokenOut | the next canonical V3 pool and its output asset; at most 3 hops; empty for a direct stock trade |
A buy routes payment → V3 hops → stock → curve or v4 pool → token; a sell is the reverse, ending in the chosen
output asset. Routes must be continuous, repeat no asset, and use pools the factory's V3 factory created; the
strategy token cannot appear in the conversion path. Every V3 leg must fill entirely. Fee-on-transfer and
rebasing assets are refused. Every failure rolls the whole route back.
Refunds are in stock. A capped final curve buy, integer rounding or a v4 price limit can leave input unused;
by then the payment has been converted to stock, so the unused part comes back as stock, in stockRefund on the
Bought event. It requires allowPartialFill, even for dust. There is no automatic continuation into the new
pool after a buy graduates the curve. A partially executed v4 sell returns unsold tokens in tokenRefund.
Set both minima on a buy. minFinalOut protects the token output; minStockReceived protects the payment-to-stock
leg, because a worse upstream rate could shrink only the refund while delivering the same capped output. A zero
minStockReceived leaves that leg unprotected.
The native router
HedgeFunV2NativeRouter.buy{value: amountIn}(params, path) wraps exactly msg.value and forwards the tokens to
the caller; params.asset and the path name the wrapped-native ERC20. sell unwraps the quoted output and returns
unsold tokens separately. Stock refunds stay stock. A rejected native payout reverts the whole trade.
Direct trades
- Curve:
buy(stockIn, minTokensOut, recipient, deadline)andsell(tokenIn, minStockOut, recipient, deadline)after approving the stock or the token to the curve. - Pool: a v4 swap on the pool key the treasury stores (
poolKey()), through any v4 router. Bound the price with a minimum output. An exact-input buy that stops at asqrtPriceLimitX96reverts; an exact-output sell is refused. Uniswap v4 pools has the details.
Getting a quote
On the curve
curve.quoteBuy(maxStockIn) → (stockSpent, tokensOut, taxTokens)
curve.quoteBuyFor(maxStockIn, to) → the same, for a recipient who may be exempt from the premium
curve.quoteSell(tokenIn) → (stockOut, taxStock)
curve.buyRateBps(), buyRateBpsFor(to) → the live buy rate, with the opening premium
stockSpent is the whole payment including the base tax; taxTokens is only the opening-premium burn. The base
stock fee is floor(stockSpent × taxBps / 10000). Show the base fee and the premium burn as two lines. A buy
that would pass the graduation target is capped: stockSpent is less than maxStockIn and the difference is
returned in stock. A quote taken on an Active curve that has graduated by the time the trade lands reverts
StageChanged; quote again.
In the pool
The hook takes the buy tax before the swap, so the pool price alone does not give a quote. Simulate the actual
router call (eth_call from the funded, approved caller with the intended deadline, stage and input) and show
the output it returns. The cost a buyer sees on a graduated pool is the tax (1% to 15%) plus the 0.20% pool fee on
the net amount, both in stock: about 1.2% at the minimum tax. A graduated pool has no opening window and no sell
spike; hook.buyRateBps(poolId) and hook.sellRateBps(poolId) both return the flat tax.
Through a route
For a multi-hop route, discover the V3 pools off chain and simulate the router call with the intended path. Display the net output, both minima, every tax and fee, and the refund denomination (stock). Refund amounts in a simulation are estimates, not a guarantee of how much input stays unused.
Pricing in USDG
A pool's price is the token in the stock. A USD price or an FDV multiplies by the stock's oracle price:
price_usdg = price_in_stock × oracle_usdg(stock)
FDV = price_usdg × supply say which supply: issued, or issued less burned
Read the token ordering from the pool key (vault.poolKey(), or treasury.stockIsCurrency0InTokenPool()); it is
not a constant across the chain's pools. The opening FDV is stock-denominated: supply × openPriceE18. An unread
number is "not fetched", never 0.
Reading state
A launch
factory.strategyCount() → how many launches
factory.strategies(id) → (token, treasury, hook, stock, creator)
factory.curves(id) → the curve
curve.status() → 0 Active · 1 Ready (transient) · 2 Graduated
curve.realStockReserve() / curve.terminalStock() → graduation progress
curve.tokenReserve(), curve.minTokenReserve() → unsold inventory and the end of the sale
hook.poolOfTreasury(treasury) → the v4 pool id after graduation
hook.liquidityVaultOf(poolId) → the vault
treasuryDeployer.strategyKindOf(salt) → the kind; lpBpsOfTreasury(treasury) the frozen LP share
Graduation progress is realStockReserve / terminalStock. The target is fixed in raw stock units at launch
(CurveLaunched carries saleBps and virtualStock).
The pool
vault.poolKey() gives the key; vault.seeded(), surplusLiquidity(), surplusTickLower() and
surplusTickUpper() describe the two positions; vault.lockedSeedTokens() the rounding residue. A pool's stock
reserve is the vault's two positions plus uncollected fees, read from the pool's own state; balanceOf(PoolManager)
holds every pool on the chain. hook.rates(poolId) returns the frozen rates; hook.accrued(poolId) and
owedProtocol, owedCreator, owedTreasury the fees waiting for a sweep.
The treasury
| view | kinds | what it tells you |
|---|---|---|
health() | all | (ok, price): whether the stock leg can trade now. (false, 0) before graduation |
unbookedStock() | all | stock that has arrived and is not yet booked; anyone may call book() |
buybackStock() | all | the stock waiting to buy the token back |
totalBurned(), totalStockSpentOnBuybacks() | all | the buy-back record |
params() | all | the frozen rule numbers and bounds |
lotCount(), lots(id), reserveUsdg(), lastSalePrice(), lastStopAt() | Lots, Percentage buy-back, Cycle | the lot book and the dip reference |
protectedGraduationStock() | Buy-back | the idle graduation capital |
engineConfig(), preview(), avgCost() | Spot, Rebalance | the frozen words, what the next execute() would do, the inventory's average cost |
riskLimits() | Rebalance | live tradable value, per-action caps, the daily cap and what is left of it |
implementation(), upgradeConfigHash(), treasuryUpgradeController() | all | the proxy's current logic and identity |
controller.proposals(treasury) | all | a pending upgrade: implementation, epoch, code hash, data hash, readyAt |
Show health() before showing a launch as trading. Show a pending upgrade proposal on the token's page: it is
the one thing about a live launch that can change.
The keeper loop
for id in 0 .. factory.strategyCount()-1:
(token, treasury, hook, stock, creator) = factory.strategies(id)
if factory.curves(id).status() != 2: continue # curve-stage: only claimFees has work
(ok, p) = treasury.health()
Lots family: simulate treasury.execute(); send if it does not revert NotDue
Buy-back: simulate treasury.buyback()
Spot, Rebalance: (due, action, amountIn) = treasury.preview(); simulate execute()
every kind: simulate treasury.buyback() when buybackStock() > 0 and the cooldown has passed
hook.sweep(poolId) when hook.accrued(poolId) is nonzero; vault.collectFees() when fees have grown
NotDue is the normal answer and costs nothing in an eth_call. execute() books pending stock as part of its
run and rolls the booking back if it ends in NotDue, so call book() on its own when stock is waiting and no
action is due. The reward is 0.1% of what the action produced, paid in that asset; sweep and collectFees pay
nothing.
Claiming fees and sweeping
Fees never move on their own. Three calls move them, and anyone may send each one.
| call | contract | what it moves | to whom | caller's reward | event |
|---|---|---|---|---|---|
claimFees(recipient) | the curve | that recipient's accrued tax, in stock | the recipient only | none | FeesClaimed(recipient, amount) |
sweep(poolId) | the hook | every pool fee claim, buy side and sell side, in stock | protocol, creator, treasury | none (sweepTipBps 0) | Swept(id, tokenBurned, stockToTreasury, stockToProtocol, stockToCreator) |
collectFees() | the vault | the pool fee accrued on both positions | stock to the treasury's buybackStock; tokens burned | none | FeesCollected(stockToTreasury, tokenBurned) |
claimFees
Works during the curve stage and after graduation. claimable(recipient) says what is waiting. A recipient the
stock issuer blocks cannot stop the other claims or curve trades; its claim waits.
sweep
The hook holds fees as ERC-6909 stock claims, one ledger per pool, at one address for every pool, so the amounts
come from accrued(poolId) and never from a balance. Each currency settles on its own leg; a leg that cannot
settle does not strand the other. On a vault pool tokenBurned is 0: no fee is ever held in the token. A sweep
does not book the treasury's share; book() or the next execute() does.
collectFees
Zero-liquidity-delta against both positions, so the principal cannot move. If the stock transfer to the treasury
is refused by the issuer, the vault keeps the stock as pending and creditPendingStock() or the next
collectFees() retries; the token burn still completes. A stock transfer refused inside v4's take reverts that
collection.
The treasury's own calls
book() costs income at the live price; execute() and buyback() pay the caller 0.1% of what they produced.
Reading state has the keeper loop.