Skip to main content

Integration

ChainRobinhood Chain mainnet, 4663 (RPC https://rpc.mainnet.chain.robinhood.com)
Explorerrobinhoodchain.blockscout.com
A launch's curve and poolquoted in the stock; the pool is Uniswap v4 on the shared hook, fee 0.20%, tick spacing 60
The treasury's stock legthe listing's stock/USDG Uniswap V3 pool, with a 600-second TWAP
Stock oracleChainlink, RHxxx / USD ÷ the USDG feed, with the rules of the three prices
Block timeabout 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​

fieldwhat it is
name, symbolthe token's
stocka listed stock; for any other, predict reverts without a reason
creatorreceives creatorBps of the collected tax, forever, and may veto a payout proposal. An EOA or a Safe
taxBps100 to 1500
creatorBps0 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, lotBpsthe rule numbers; meaning and bounds depend on the strategy
bandBpsPerHourthe staleness band, 0 to bandCeiling(stock)
nonceseparates two launches of the same symbol by the same creator; part of the salt
maxFeethe most the launcher will pay as launch fee
expectedOpenPriceE18the 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/icons with the PNG, signed by a wallet that holds at least the launch fee in ETH on the chain. The signed message is

    Hedgefun icon upload
    Host: hedgefun.trade
    SHA-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.

fieldmeaning
idthe launch id on this factory
assetbuy: the payment ERC20; sell: the desired output ERC20
amountInbuy: payment in the asset's raw units; sell: strategy tokens
minStockReceivedbuy: the least stock the payment route must produce, including stock later refunded; ignored on sells
minFinalOutthe least final output after every swap and tax, never scaled down on a partial fill
deadlinelast permitted timestamp
expectedStage0 Active or 2 Graduated, checked before funds move
allowPartialFillaccept a stock refund on a buy, or a token refund on a sell
Hop.pool, Hop.tokenOutthe 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) and sell(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 a sqrtPriceLimitX96 reverts; 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​

viewkindswhat it tells you
health()all(ok, price): whether the stock leg can trade now. (false, 0) before graduation
unbookedStock()allstock that has arrived and is not yet booked; anyone may call book()
buybackStock()allthe stock waiting to buy the token back
totalBurned(), totalStockSpentOnBuybacks()allthe buy-back record
params()allthe frozen rule numbers and bounds
lotCount(), lots(id), reserveUsdg(), lastSalePrice(), lastStopAt()Lots, Percentage buy-back, Cyclethe lot book and the dip reference
protectedGraduationStock()Buy-backthe idle graduation capital
engineConfig(), preview(), avgCost()Spot, Rebalancethe frozen words, what the next execute() would do, the inventory's average cost
riskLimits()Rebalancelive tradable value, per-action caps, the daily cap and what is left of it
implementation(), upgradeConfigHash(), treasuryUpgradeController()allthe proxy's current logic and identity
controller.proposals(treasury)alla 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.

callcontractwhat it movesto whomcaller's rewardevent
claimFees(recipient)the curvethat recipient's accrued tax, in stockthe recipient onlynoneFeesClaimed(recipient, amount)
sweep(poolId)the hookevery pool fee claim, buy side and sell side, in stockprotocol, creator, treasurynone (sweepTipBps 0)Swept(id, tokenBurned, stockToTreasury, stockToProtocol, stockToCreator)
collectFees()the vaultthe pool fee accrued on both positionsstock to the treasury's buybackStock; tokens burnednoneFeesCollected(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.