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
minAmountsOutandpoolTokensalign index-by-index - EIP-712
ExitProportionalIntentbuilder, signing, submission, and polling - LP balance helper (the pool contract itself is the LP token)
- Tests for ordering, the unsigned
poolTokensbody field, and slippage math
How to use
- 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.
- Optionally type one line of placement guidance first, e.g.
Put this under src/integrations/ryze/. Then: - Hit the Copy prompt button below and paste the prompt as your message.
- 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.
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.