Skip to content

Prompt: Exit Liquidity

Lets users withdraw liquidity: burn LP shares and receive all pool tokens proportionally. The generated code quotes the exit with an on-chain view call (no HTTP quote exists for exits), signs an EIP-712 ExitProportionalIntent, submits, and polls. No token approval is needed for exits.

What the agent will build:

  • Exit quoting via WeightedPoolQueries.queryProportionalExit (plus a “withdraw all” variant)
  • Canonical pool-token ordering so minAmountsOut and poolTokens align index-by-index
  • EIP-712 ExitProportionalIntent builder, signing, submission, and polling
  • LP balance helper (the pool contract itself is the LP token)
  • Tests for ordering, the unsigned poolTokens body field, and slippage math

How to use

  1. Open your AI coding agent (Claude Code, Cursor, Windsurf, Copilot, Codex, … — any model works) inside the repository you want to integrate. The agent needs zero prior knowledge of Ryze; the prompt contains everything.
  2. Optionally type one line of placement guidance first, e.g. Put this under src/integrations/ryze/. Then:
  3. Hit the Copy prompt button below and paste the prompt as your message.
  4. When the agent finishes, hold the result to the acceptance checklist at the end of the prompt — every box should check.

Prerequisites

None for running the prompt itself. To exercise the generated code you’ll need a Base Sepolia RPC URL and a test wallet holding testnet USDC/WETH plus a little ETH for the one-time approve transaction. All endpoints and addresses are already embedded in the prompt with Base Sepolia defaults; mainnet values are included for the switch.

The prompt

The prompt is fully self-contained — one click copies all of it.

View raw
Preview the full prompt text
md
You are integrating the **Ryze protocol** (an intent-based AMM on Base) into this
repository so users can exit a Ryze pool: burn LP shares and receive all pool tokens
proportionally. Implement it in this repository's existing language, framework, HTTP
client, and conventions — study the repo first and match its style.

## How Ryze exits work (authoritative)

The pool contract itself is the ERC-20 LP token; exiting burns `sharesIn` and pays out all
pool assets pro-rata. **No approval is needed** — shares are burned directly by the pool,
not pulled by the router. The user never sends the exit transaction:

1. **Quote** the exit with an on-chain view call (there is no HTTP exit-quote endpoint).
2. Read the user's **exit nonce** from the `MultiHopRouter` contract.
3. Have the user **sign an EIP-712 `ExitProportionalIntent`**.
4. **POST** the signed intent to the Ryze execution relayer.
5. **Poll** until `confirmed` or `failed`.

The relayer batches intents, attaches all oracle data (Pyth + signed CEX prices), executes
on-chain, and pays gas. **Do not** fetch/attach prices, call `execute*` contract functions,
or build raw calldata.

## Configuration

Read from environment/config (never hardcode):

| Env var | Default (Base Sepolia, chainId 84532) | Base mainnet (chainId 8453) |
|---|---|---|
| `RYZE_CHAIN_ID` | `84532` | `8453` |
| `RYZE_RELAYER_URL` | `https://sepolia.relayer.ryze.pro/api/v1` | `https://mainnet.relayer.ryze.pro/api/v1` |
| `RYZE_API_URL` | `https://sepolia.api.ryze.pro/api` | `https://mainnet.api.ryze.pro/api` |
| `RYZE_MULTIHOP_ROUTER` | `0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A` | `0x8e20A1534a204DE569E9d62D410c19795b81DF70` |
| `RYZE_POOL_QUERIES` | `0x1a7B7D9071c30935448b6cC3d114c0c794d0a3C0` | `0xD4b5DD638d09aB7b6Eb4Ec3490F02A96d0DD4100` |
| `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC |

Pool discovery: `GET {RYZE_API_URL}/pools/list` (pools with ordered
`tokens[{address,weight,decimals}]`). User positions:
`GET {RYZE_API_URL}/portfolio/pools/{address}` or simply
`ERC20(pool).balanceOf(user)` for the share balance.

## Step 1 — Quote (on-chain view call)

`eth_call` on the `RYZE_POOL_QUERIES` contract (`WeightedPoolQueries`).
⚠ The deployed contract returns a **struct-wrapped array** — declare the ABI output as
`tuple(uint256[] amountsOut)`, NOT bare `uint256[]` (decoding as `uint256[]` throws;
verified on Base Sepolia):

```solidity
function queryProportionalExit(address pool, uint256 sharesIn)
    external view returns (ExitResult result); // struct { uint256[] amountsOut; }
```

(There is also `queryProportionalExitForUser(address pool, address user)` returning the
token ordering alongside amounts — useful for a "withdraw all" feature.)

**Token order matters.** `amountsOut[i]` corresponds to the pool's asset list order —
obtain it from the pool's `tokens[]` in `GET {RYZE_API_URL}/pools/list` or the pool
contract's `assets()`; call this ordered list `poolTokens`.

**Where the user's shares live:** when `MultiHopRouter.poolToGaugeVault(pool)` (view →
`address`) is non-zero — true for the listed pools — LP from joins is auto-staked in the
LPGaugeVault, so the exitable position is
`pool.balanceOf(user) + gauge.getEligibleBalance(user)` (the exit burns from the gauge
balance first; still no approval needed).

Derive `minAmountsOut[i] = floor(amountsOut[i] × (10000 − slippageBps) / 10000)` with
big-integer math (never floats) — then derate index 0 for the **flat intent fee**
(deployed behavior, verified live): `MultiHopRouter.intentFee()` (view → `uint256`) is a
flat USD fee in WAD (currently 1e16 = $0.01) taken in units of **pool asset 0** out of the
exit proceeds; the contract enforces `amountsOut[0] ≥ minAmountsOut[0] + fee`. So:
`minAmountsOut[0] = max(1, minAmountsOut[0] − ceilDiv(intentFee × 10^decimals(asset0), blendedPrice(asset0)))`
(price for asset0 from any Router quote's `prices[]`). Warn when the exit's asset-0
proceeds are worth < ~$10 — the fee alone then exceeds typical slippage.

## Step 2 — Nonce

Exits have their own nonce mapping. `eth_call` on `RYZE_MULTIHOP_ROUTER`:

```solidity
function exitProportionalNonces(address user) external view returns (uint256);
```

Submitted nonce must EXACTLY equal this value. Fresh read before signing; never two
in-flight exit intents for the same user.

## Step 3 — EIP-712 signature

Domain:

```json
{ "name": "MultiHopRouter", "version": "1", "chainId": <RYZE_CHAIN_ID>, "verifyingContract": "<RYZE_MULTIHOP_ROUTER>" }
```

Type (exact field order — note `poolTokens` is absent):

```text
ExitProportionalIntent(address user,address pool,uint256 sharesIn,uint256[] minAmountsOut,address recipient,uint256 deadline,uint256 nonce)
```

`recipient` receives the withdrawn tokens (usually the user); `deadline` = unix seconds ≈
`now + 1800`. Sign with the user's key and submit the 65-byte signature as one hex
string (`r‖s‖v`, `v` = 27/28; low-`s` required). ERC-1271 smart-contract wallets (e.g.
Safe) submit their contract signature bytes instead.

## Step 4 — Submit

`POST {RYZE_RELAYER_URL}/intents/submit`:

```json
{
  "type": "exitProportional",
  "user": "0x…",
  "pool": "0x…",
  "sharesIn": "1000000000000000000",
  "minAmountsOut": ["498000000000000000", "1245000000"],
  "poolTokens": ["0xWETH…", "0xUSDC…"],
  "recipient": "0x…",
  "deadline": 1789000000,
  "nonce": 0,
  "signature": "0x…"
}
```

⚠️ `poolTokens` is REQUIRED in the HTTP body (the relayer uses it off-chain for oracle
price coverage) but is **NOT part of the signed EIP-712 message**. It must be the same
length and order as `minAmountsOut` (≥ 2 entries). `sharesIn` must be > 0 and ≤ the user's
LP balance. All amounts are base-10 strings.

Responses: HTTP **202** `{ "success": true, "intentId": "0x…", "status": "pending" }`
(accepted, idempotent on resubmit); HTTP **400** with `message` (bad signature / nonce
mismatch with hint / expired deadline / array mismatch / missing `poolTokens`); HTTP
**429** rate limit (~100 req/min/IP) — back off.

## Step 5 — Poll status

`GET {RYZE_RELAYER_URL}/intents/{intentId}` every ~2s until `confirmed`/`failed`
(typical 3–12s; timeout ~90s, return `intentId` for resumable polling). `failed` carries a
decoded revert reason in `error` and has no on-chain effect (shares stay with the user).

## What to build

1. A **Ryze exit module** in this repo's conventions:
   - `getExitQuote(pool, sharesIn, slippageBps)` → ordered `poolTokens`, expected
     `amountsOut`, derived `minAmountsOut` (fee-derated on index 0; plus a
     `getExitQuoteForUser(pool, user)` "withdraw all" variant)
   - `getLpPosition(pool, user)` — wallet LP + gauge `getEligibleBalance`
   - `buildExitIntent(...)` → unsigned EIP-712 payload (domain + types + message)
   - `submitExitIntent(intent, signature)``intentId`
   - `waitForIntent(intentId, {pollMs, timeoutMs})`
   - convenience `exit(...)` composing the flow
2. **Config** via the env vars above.
3. **Typed errors** (insufficient shares, nonce conflict, validation 400s, timeout).
4. **Tests**: mock the relayer HTTP and the two view calls; assert EIP-712 type string and
   array encoding, that `poolTokens` is in the body but not the signed message and aligns
   with `minAmountsOut`, integer slippage math, signature hex encoding, polling state machine.

## Acceptance checklist

- [ ] No oracle/price code, no direct contract execution, no raw calldata, and no LP-share
      approval step (not needed for exits).
- [ ] `poolTokens` in submit body only, aligned index-by-index with `minAmountsOut`.
- [ ] Token ordering taken from the pool's canonical asset order, never assumed.
- [ ] `queryProportionalExit` decoded as `tuple(uint256[] amountsOut)`.
- [ ] `minAmountsOut[0]` derated by `intentFee()` in asset-0 units (floor 1).
- [ ] Position read as wallet + gauge (`poolToGaugeVault`/`getEligibleBalance`).
- [ ] `exitProportionalNonces` used; fresh read; in-flight serialization.
- [ ] Amounts as big-integer-backed strings; every `minAmountsOut[i]` computed exactly.
- [ ] 202/400/429 and `pending/confirmed/failed` handled and tested.
- [ ] Everything configurable via env; defaults point at Base Sepolia.