
LP positions you can trade like tokens. One token, two ways to trade it. Built on Uniswap v4.
lpTOKEN.fun makes a Uniswap v4 LP position tradable like a token. That gives traders two ways to trade one token. Hold the token itself for price. Or hold its lpTOKEN, a share of the pool. It still moves with price, and it also collects the pool's trading fees into its NAV. One asset, two positions, and you pick the side. Under the hood, a vault owns one full range v4 position, and the vault contract is itself the lpTOKEN ERC-20. Every share is a claim on the same position, so shares are fungible. Mint with both sides, zap in with one asset, redeem anytime, or send it like any other token. Before ETHOnline, lpTOKEN.fun was live on Robinhood Chain and Base, including stock token markets, so lpAAPL is a position you can hold instead of AAPL. During the event we took the stack to Arc mainnet. Arc's native currency is USDC, so an lpTOKEN there is dollar denominated. That needed a new zap router: on Arc, native USDC and ERC-20 USDC are one balance with different decimals, and there is no WETH style wrapper. More than a dozen new lpTOKEN markets went live during the event, including the first ones on Arc. Ethereum mainnet is still in development as we submit. Our plan is to ship it before the hackathon ends, so existing v4 pools on Ethereum can get an lpTOKEN too.
The whole problem is making a Uniswap v4 position behave like a token. Everything else follows from that. A market is one static fee v4 pool. On a platform launch the pool is opened behind a hook that declares exactly one permission, beforeInitialize, and that function always reverts. v4 skips the callback when the caller is the hook itself, so the launchpad's own atomic initialization is the only path that can open the pool, and no other hook logic exists anywhere in the hot path. A platform market then holds two positions in that one pool: a permanent one-sided launch position carrying the initial inventory, and the vault position, which is full range and bounded away from the extreme ticks so nobody can freeze it. Markets for tokens that launched elsewhere skip the launch position entirely and get the vault position alone. The vault owns its position directly through the PoolManager singleton, and the vault contract is itself the lp{SYMBOL} ERC-20 with permit. No NFT, no wrapper in between. That is what makes the claim fungible, and it means a share is a pro rata claim on principal, collected fees, idle balances and the launch NAV together. Minting takes both sides. The vault syncs launch fees, collects the position's fees, then scales every component of the existing portfolio by one gross-share ratio: the proportional slice goes into the position, the remainder stays idle in the vault. Liquidity per share and idle assets per share both stay unchanged, so a mint can't move pre-existing NAV into the pool at a price the minter picked. Whatever isn't used is refunded in the same transaction. Redeeming burns shares and returns the proportional claim on both currencies. If you'd rather come in or leave with one asset, the ZapRouter handles it: zap in from either leg of the pair directly against the vault pool, or from the native currency or the canonical stablecoin across up to three hookless static fee hops, and zap out the same way. It runs inside PoolManager.unlock, and every entrypoint has a permit variant so there's no separate approval transaction. Fees are the reason to hold the lpTOKEN, so the vault position's earnings simply stay: 100% of them accrue to holder NAV, with no management fee and no performance fee. The 0.30% share fee on public mint and redeem is taken in shares to the treasury and never comes out of LP earnings. On platform markets the separate launch position splits its fees, 40% to the creator and 60% to NAV on the target side, and 40% creator, 20% NAV, 40% treasury on the counter side. compound() is permissionless and swapless. Anyone can call it, it reinvests only matched balances, and it's capped: the deployable amount for an interval is half of the pool's own fee ratio applied to the protocol owned liquidity live in the pool right now, read through StateLibrary, behind a ten minute cooldown. So it can't be used to walk the pool. One-sided or capped residue stays idle and stays redeemable. Two details worth calling out. Vaults are deployed with cloneDeterministicWithImmutableArgs, and the pool binding and fee source are part of those immutable arguments, so the vault's own address commits to what it is bound to and nothing can be repointed later. And the minimum assessable amount is 334 shares, derived in the contract rather than written down: it's the smallest amount whose exact 30 basis point fee is at least one raw share, and every fee rounds up, so an operation can't be fragmented into dust to dodge the charge. Why v4 specifically: the singleton lets the vault hold a position without going through a position NFT, unlock gives the router flash accounting for multi-hop entry and exit, an immutable static fee is what makes the compound cap computable at all, and hooks let us gate initialization without paying for callbacks we never use. This design doesn't really port back to v3. There is no USD numeraire and no oracle anywhere in the Solidity. It counts raw amounts of currency0 and currency1 and never asks what anything is worth in dollars. That's also why the same contracts run on any EVM chain with a v4 deployment, whatever the native currency happens to be. The stack: Foundry and Solidity 0.8.36 with OpenZeppelin and the v4 core and periphery libraries for the contracts. Next.js 16, React 19, viem, wagmi and TanStack Query for the app, and our own indexer writing to PostgreSQL through Prisma, which serves charts, holder PnL and LP activity without depending on a third party subgraph. Everything above existed before ETHOnline, running on Robinhood Chain and Base. During the event we deployed the stack to Arc mainnet and added ZapRouterArc. On Arc, native USDC (18 decimals) and ERC-20 USDC (6 decimals) are one balance, so the router converts accounting units instead of wrapping or unwrapping anything. That work is in the public repo's commit history. Ethereum mainnet is in development, and we plan to ship it before the event ends.