
Trelp
Tranched Uniswap v4 liquidity, with a 1inch Aqua buffer that never leaves the vault.
Demo
About
Trelp splits a single Uniswap v4 liquidity position into two tradeable claims with different risk profiles. THE PROBLEM Being an LP is one blended bet. You earn trading fees, you lose money when the pair moves against you, and you find out the net at the end. There is no way to buy only the steady part or only the levered part. So two kinds of capital stay out: treasuries that need predictable yield will not accept a position that can be down 20% because ETH moved, and traders who want leveraged exposure to fee income have to borrow it elsewhere and manage that separately. Both want the same position. They want different slices of it. HOW IT WORKS Depositors choose a side. Senior is paid first at settlement and earns a capped return, protected by junior's capital sitting underneath it. Junior absorbs losses before senior feels anything, and in exchange keeps the entire upside above senior's capped claim. Both sides hold ERC-20 claim tokens, so a position can be sold before the epoch ends. Senior's rate is not configured in advance. It is derived when the deposit window closes, from how much of each side actually arrived. More junior capital means a thicker cushion, so senior needs to be paid less for the risk. The derivation requires junior to beat an unlevered LP at any fee yield, which makes the fee term cancel out, so the bound holds regardless of how the epoch performs. Past roughly 50% junior there is no rate that leaves both sides better off than simply LPing, and the vault refuses to start rather than sell terms that do not work. A curator manages the position: picks the price range and moves it as the market moves. Those powers are deliberately narrow. The curator never holds depositor funds, cannot change terms once the epoch starts, is rate limited by a cooldown, and is blocked from moving the range at all once the position is under stress. Every move has its cost measured and recorded. The protocol never computes impermanent loss. At settlement it holds one number, the cash balance after unwinding, and pays senior first up to its claim. Everything (fees, IL, adverse selection, unwind cost) is already inside that number. Attributing it per user would need path-dependent accounting and a counterfactual price oracle, and would change nobody's payout. THE TWO INTEGRATIONS Uniswap v4: a custom hook gates the pool to the vault's own position, overrides the LP fee from a live solvency reading, and refuses swaps that would push more of the falling asset onto the vault while still accepting trades that reduce risk. A dynamic fee is symmetric and cannot skew, so a directional refusal is the only way to decline one side. A normal pool has no way to say no, which is exactly how an LP ends up buying all the way down. 1inch Aqua: part of junior's capital works as a standing bid below spot. Aqua takes no custody, so that capital is still sitting in the vault backing senior while it quotes. It buys at a discount in calm markets, widens as the cushion thins via an Extruction target that reads vault solvency at swap time, and stops bidding entirely when coverage breaks. Anyone can then revoke it permissionlessly, turning it back into pure loss absorption. Both venues read the same coverage number from the same contract. One view of risk, applied in two places. WHAT IS BUILT Full protocol in Solidity with 98 tests across 11 suites, plus an opt-in mainnet fork suite that runs the whole stack against the real v4 PoolManager and a real Aqua registry. Live deployment on Base Sepolia with a working frontend: deposits, claim balances, live coverage, on-chain event history, and redemption. A full epoch on a mainnet fork, with ETH falling 45%: coverage goes from 42.9% to negative, the pool fee widens 30 to 90 bps, the hook refuses a swap, the buffer is revoked, and the vault settles. Junior loses everything, senior finishes down 0.98%. That is the honest shape of the product. Junior is a cushion, not a guarantee. KNOWN LIMITS Senior's coupon is paid from realised gain, so a flat epoch returns principal and nothing else. A CLO-style interest waterfall is the fix and is not built. There is no hedge during a drawdown and no early settlement. One position per vault.
How it's made
UNISWAP V4 HOOK The hook is what makes the pool defensible. Gates liquidity so only our vault can LP in, overrides the LP fee live off the vault's solvency, and refuses swaps that would dump more of the falling asset on us while still taking trades that reduce risk. A dynamic fee is symmetric and can't skew, so a directional refusal is the only way to decline one side. A normal pool has no way to say no, which is how an LP ends up buying all the way down. v4 encodes hook permissions in the address, so we mine it with CREATE2 until it ends in 0x880. Pool also needs DYNAMIC_FEE_FLAG or the fee override is silently ignored. 1INCH AQUA ship() takes no custody, the tokens stay in your wallet. That's the whole reason this works: junior's capital is a live standing bid and a loss-absorbing reserve at the same time, because it never leaves the vault. The buffer is a SwapVM program, quote only. A two sided maker would hold inventory correlated with the exact drawdown the buffer exists to survive. XYCConcentrateSwap derives virtual reserves from its bounds, so a zero risky balance still quotes a working bid. Aqua strategies are immutable once shipped, so a spread tracking live coverage can't be a strategy parameter. Extruction fixes that: it hands the swap registers to any contract mid program, so our adjuster reads the vault at swap time and rewrites the output. Coverage-aware pricing against the officially deployed SwapVM, no redeploy. We also had to anchor the bid to an oracle. With an unanchored curve the risk policy made senior worse off (-15.43% vs -15.50%), because it underpaid for the risky asset so refusing fills gave up as many gains as losses. Anchored, the same crash went -1.24% vs -15.44%. Those two demo paths differ by one constructor argument. THE ANNOYING PART v4-core pins solc 0.8.26, Aqua and SwapVM pin 0.8.30. No single file can import both. The vault talks to both through hand written interfaces, every test suite takes one venue real and mocks the other, and the one place both run together uses deployCode so no type crosses the version line. activate() ships to Aqua and mints the v4 position in the same call, and that path had never actually run until we wrote a combined test for it. STACK Solidity, Foundry, Next.js 16, viem, Privy. Live on Base Sepolia, verified against forked mainnet with the real PoolManager and a real Aqua registry. 98 tests. TWO THINGS THAT BIT US Asset ordering. Pool price is currency1 per currency0, so which range bound exposes you to the risky asset flips with token ordering. We had it as a constructor bool. Get it wrong and the halt inverts silently, refusing the safe side and allowing the dangerous one. Derived from the pool key now. settle() needed the risky balance at zero, but unwinding an in-range position returns both tokens and nothing could convert them. redeem needs Settled, cancel needs Subscription. Funds would have been stuck permanently. Tests missed it because the harness was impersonating the vault to dump inventory. Added liquidate(), permissionless and floored against the oracle.