--- name: soulzap-up33 description: Adds SoulZap support to up33 - entering up33 Slipstream concentrated liquidity or vAMM pairs on Robinhood Chain with a single token, and staking a vAMM pair into its gauge in the same transaction. Use this whenever the task involves an up33 pool or an up33 gauge stake. Companion to soulzap-integration.txt, which must be read first. --- # Integrate SoulZap into up33 > **Read `soulzap-integration.txt` first** - it carries the flow, the rules and every `lpData` > variant. This file only adds what is specific to up33. ## The goal One transaction that takes a single token the user already holds and leaves them in an up33 position: swap, add liquidity, and for a vAMM/sAMM pair stake into its gauge - atomic, reverting as a whole if any leg fails. This is an extra path into positions up33 already has. The existing manual add-liquidity and stake flows stay exactly as they are. ## Start in the codebase Find these before writing anything, and reuse them: - **The add-liquidity path that exists today** - `addLiquidity`, `mint`, `increaseLiquidity`, the router and position-manager calls. The zap swaps out the calldata, not the surrounding UX. - **Where pools come from** - factory reads, a subgraph, or a static list - and how the app already reads a pool's `tickSpacing`, current tick and stable flag. Reuse that; do not add a second source. - **The wallet helpers** - send, wait-for-receipt, and the ERC-20 approve/allowance helper. - **The slippage and deadline settings the user already controls.** Feed those into the request rather than introducing a second control. - **The token selector**, if there is one. Paying with any held token is the point of the zap; if the UI only offers pool tokens, that list is what needs widening. Then make the smallest change that adds the path: build `lpData`, `POST /approve`, `POST /zap`, send `txData` through the helper that already exists. No new dependency - `fetch` is enough. ## What we know about up33 - A ve(3,3) DEX on **Robinhood Chain** only - send `"chain": "robinhood"` (or `4663`). - An Aerodrome fork. The AMM side is Solidly, the concentrated-liquidity side is Slipstream. Neither is a Uniswap V2 or V3 deployment, but `solidly` and `univ3` are still the right `lpType`s. - Emissions run through a Voter. vAMM/sAMM gauges stake an ERC-20; CL gauges take a position NFT, which is why only the former can be zapped into (see below). ## lpType mapping | up33 pool | `lpType` | Seed address | |---|---|---| | Concentrated liquidity (Slipstream) | `univ3` | pool address -> `pool` | | vAMM / sAMM pair | `solidly` | pair address -> `lpAddress` | Slipstream pools are keyed by tick spacing rather than a fee tier, and its position manager takes a different `mint` encoding than Uniswap V3. The API resolves that from the position manager's bytecode, so `univ3` is correct and there is nothing extra to pass. ## Addresses | Used by | Field | Address | |---|---|---| | `univ3` | `positionManager` | `0x07F44c47743A2f36414A82b9F558ECFCf0EEdCEf` | | `solidly` | `router` | `0xf5198743240fAC98db71868F34c70139b1eb0474` | Used for discovery, not in `lpData`: | Contract | Address | |---|---| | CL factory | `0x1ac9dB4a2608ba45D6127B1737949b51Bb54B7F3` | | AMM factory | `0xFA5429AEBa338BEa2BFcc1b9a889862Ee395bc28` | | Voter | `0x7F749fDD351C1Ceed82d76d7699CB631Eb8332a7` | | WETH | `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73` | If the codebase already holds these, use its copy rather than pasting them in again. ``` clFactory.getPool(tokenA, tokenB, int24 tickSpacing) -> CL pool ammFactory.getPool(tokenA, tokenB, bool stable) -> vAMM / sAMM pair ``` Both differ from the Uniswap signatures: the CL factory takes an `int24 tickSpacing` where Uniswap V3 takes a `uint24 fee`, and the AMM factory is `getPool`, not `getPair`. ## `univ3` - Slipstream ```json { "lpType": "univ3", "fromToken": "0x...", "fromAmount": "1000000", "positionManager": "0x07F44c47743A2f36414A82b9F558ECFCf0EEdCEf", "pool": "0x...", "tickLower": 94800, "tickUpper": 107000 } ``` Tick rules: - `tickLower` and `tickUpper` must be multiples of the pool's `tickSpacing`. Read it from the pool; deployed spacings include 1, 10, 60, 100, 200 and 2000. - A range that does not contain the pool's current tick mints a single-sided position. To deposit both tokens, straddle `slot0().tick`. - up33's pools sit at large ticks - WETH/UP near 100,900, WETH/USDG near -200,970 - so ranges have to be derived from the current tick rather than hardcoded. If the app already has range presets, reuse their maths and only round to spacing: ```ts const [, tick] = await pool.slot0() const spacing = await pool.tickSpacing() const half = 6000 // 0.55x to 1.8x in price const tickLower = Math.floor((tick - half) / spacing) * spacing const tickUpper = Math.ceil((tick + half) / spacing) * spacing ``` Pass `tokenId` to add to an existing position; its own range is used. ## `solidly` - vAMM and sAMM ```json { "lpType": "solidly", "fromToken": "0x...", "fromAmount": "1000000", "lpAddress": "0x...", "router": "0xf5198743240fAC98db71868F34c70139b1eb0474" } ``` ## `protocolData` - gauge staking ```json { "protocol": "Up33", "gauge": "0x..." } ``` Valid with `solidly` only. The ERC20 gauge exposes `deposit(uint256 amount, address recipient)`, so the zap stakes the pair and credits the user in one transaction. Not valid with `univ3`. The CL gauge takes a position NFT through `deposit(uint256 tokenId)`, which credits `msg.sender` and takes no beneficiary, so the router cannot stake on the user's behalf. The API rejects the request. Staking a CL position stays two transactions from the owner's wallet - the flow the app already has: ``` nft.approve(gauge, tokenId) gauge.deposit(tokenId) ``` Omit `protocolData` to leave the LP token in the user's wallet. An up33 stake is an internal balance, not a receipt token, so no staking token appears in the outputs. ### Resolving and checking the gauge ``` voter.gauges(pool) -> gauge address, or the zero address if the pool has none voter.isAlive(gauge) -> false once the gauge is killed ``` `deposit` reverts `NotAlive()` on a killed gauge. The API performs the same check and rejects it at quote time. Skip the protocol step when the gauge is the zero address or not alive, and zap the pool without it. ## Worked example - native ETH into a vAMM pair, staked Only the request objects; approve and zap exactly as the base guide does. ```ts const lpData = { lpType: 'solidly', fromToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // native ETH, nothing to approve fromAmount: '5000000000000000', // 0.005 ETH lpAddress: '0x8697bB63743678640710b1270E98772a29642B3c', // vAMM-WETH/UP router: '0xf5198743240fAC98db71868F34c70139b1eb0474', slippage: 1, } const gauge = await voter.gauges(lpData.lpAddress) const stakeable = gauge !== ZERO && await voter.isAlive(gauge) const protocolData = stakeable ? { protocol: 'Up33', gauge } : undefined // omit the key if not staking ``` ## Reading the response What is specific to up33: | Destination | `lpQuote.lpAddress` | What the user receives | |---|---|---| | `univ3` | the position manager | a position NFT, minted straight to `recipient` | | `solidly` | the pair | the LP token | | `solidly` + `protocolData` | the pair | nothing in the wallet - the stake is a gauge balance | - `lpQuote.token0` and `token1` carry the amount deposited per side. Neither destination sets `toAmountEstimate`. - For `univ3` the NFT is never an output, so it does not appear in `outputs` and no token id is returned in the quote. Read it from the `Transfer` / `IncreaseLiquidity` event in the receipt, or from the position manager after the transaction. Whatever the range could not take is returned to `recipient` as token0 and token1. - For `solidly` without a protocol step, the LP output carries a minimum, so the transaction reverts if the deposit mints less than quoted. - For `solidly` with a protocol step, that LP minimum is released, because the gauge spends the LP before the router checks its balance. The remaining on-chain bounds are the router's own `amountAMin`/`amountBMin` on the two deposited amounts. - `protocolQuote` is `{ "protocol": "Up33" }` and carries no further fields. ## Errors | Message | Cause | |---|---| | `Gauge ... stakes ..., not ...` | The gauge belongs to a different pool than this `lpData` produces. | | `... is not an up33 ERC20 gauge` | A CL gauge was passed in `protocolData`. | | `Gauge ... is no longer alive` | `voter.isAlive(gauge)` is false. | | `Ticks must be multiples of tickSpacing ...` | Ticks not rounded to the pool's `tickSpacing`. | | `fromAmount is too small to provide both sides of this range` | CL deposit too small to split. | | `execution reverted: InsufficientAmountB()` (`0x34c90624`) | Solidly router: `fromAmount` too small, one side rounds to dust. |