Skip to content

Prompt: Proportional Liquidity Join

Lets users add liquidity with all pool tokens in ratio — the fee-free way to LP on Ryze. The generated code computes proportional amounts, quotes, checks every token’s allowance, signs an EIP-712 JoinProportionalIntent, submits, and polls.

What the agent will build:

  • Proportional amount calculation from a single user-entered amount
  • Join quoting with minPoolTokensOut derivation (and the minSharesOut signed-field mapping)
  • Multi-token allowance detection returning the full list of missing approvals
  • EIP-712 JoinProportionalIntent builder with correct array encoding
  • Submission, polling, and tests for the array/naming pitfalls

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 add liquidity to a Ryze pool by depositing **all pool tokens in
proportion** (fee-free) and receive LP shares. Implement it in this repository's existing
language, framework, HTTP client, and conventions — study the repo first and match its style.

## How Ryze proportional joins work (authoritative)

Depositing all pool tokens at the pool's current ratio incurs **no fees** (unlike
single-asset joins). The pool contract itself is the ERC-20 LP token. The user never sends
the join transaction:

1. Compute proportional amounts and **quote** via the Ryze Router service.
2. Ensure the user has **approved** the `MultiHopRouter` contract for **every** input token.
3. Read the user's **proportional-join nonce** from the `MultiHopRouter` contract.
4. Have the user **sign an EIP-712 `JoinProportionalIntent`**.
5. **POST** the signed intent to the Ryze execution relayer.
6. **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_ROUTER_URL` | `https://sepolia.router.ryze.pro` | `https://mainnet.router.ryze.pro` |
| `RYZE_API_URL` | `https://sepolia.api.ryze.pro/api` | `https://mainnet.api.ryze.pro/api` |
| `RYZE_MULTIHOP_ROUTER` | `0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A` | `0x8e20A1534a204DE569E9d62D410c19795b81DF70` |
| `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC |

Pool discovery: `GET {RYZE_API_URL}/pools/list` → pools with `tokens[{address,weight,decimals}]`.
Reference pools:
- Sepolia: WETH-USDC `0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723`, cbBTC-USDC `0xaC522dD3B13c8b2F4b765d100b1Ea13493Cf55bA`
- Mainnet: WETH-USDC `0x22f902cEfcF8b0bEc6489Cb8ac11FdDa9B2aF125`, cbBTC-USDC `0x40F3DAaE59BfE03f9Fb019Bb089Bb0C381DE27Cf`

## Step 1 — Quote

`POST {RYZE_ROUTER_URL}/join-quote` with **all** pool tokens present:

```json
{
  "poolAddress": "0x…",
  "tokensIn": [
    { "token": "0xWETH…", "amount": "500000000000000000" },
    { "token": "0xUSDC…", "amount": "1250000000" }
  ]
}
```

When every pool token is included, the response has `joinType: "proportional"` and zero
fees. Key fields: `sharesOut` (18-decimal string), `tvl`, `shareOfPool`, `prices[]`
(per-token USD prices in WAD — use these to compute the proportional counterpart amount
from a user-entered primary amount). Errors: 400 invalid params, 404 unknown pool.

UX guidance to implement: let the user enter ONE token amount, derive the other token
amount(s) from the current pool ratio (pool balances or the quote's `prices[]`), then quote
to confirm and get `sharesOut`. If the ratio drifts before submission, the intent still
executes proportionally — surplus tolerance is captured by `minSharesOut`.

Deriving `minPoolTokensOut` MUST account for the **flat intent fee** (deployed behavior,
verified live): `MultiHopRouter.intentFee()` (view → `uint256`) is a flat USD fee in WAD
(currently 1e16 = $0.01) converted to units of **`tokensIn[0]`** at oracle price
(`feeTok0 = ceilDiv(intentFee × 10^decimals(tokensIn[0]), blendedPrice(tokensIn[0]))`,
prices from the quote's `prices[]`) and deducted from `amountsIn[0]` before the join —
which caps the proportional ratio and thus the shares. Quotes exclude it, so:
`minPoolTokensOut = floor(sharesOut × (amountsIn[0] − feeTok0) / amountsIn[0] × (10000 − slippageBps) / 10000)`
with big-integer math; must be > 0. Warn (or refuse) when `amountsIn[0]` is worth < ~$10 —
the fee alone then exceeds typical slippage and the intent reverts `InvalidSlippage()`.

## Step 2 — Approvals (plural)

Every token in `tokensIn` needs `ERC20.allowance(user, RYZE_MULTIHOP_ROUTER) ≥ amount`.
Build the allowance check to return the full list of missing approvals so the UI can
request them all before signing.

## Step 3 — Nonce

Proportional joins have their own nonce mapping. `eth_call` on `RYZE_MULTIHOP_ROUTER`:

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

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

## Step 4 — EIP-712 signature

Domain:

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

Type (exact field order):

```text
JoinProportionalIntent(address user,address pool,address[] tokensIn,uint256[] amountsIn,uint256 minSharesOut,address recipient,uint256 deadline,uint256 nonce)
```

⚠️ Naming trap: the **signed field is `minSharesOut`**, but the **HTTP body field is
`minPoolTokensOut`** — same value, two names. Sign `minSharesOut = minPoolTokensOut`.

`tokensIn` and `amountsIn` must be same length, same order, ≥ 2 entries, no duplicate
tokens, every amount > 0. `recipient` receives LP shares; `deadline``now + 1800`
(unix seconds). 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 5 — Submit

`POST {RYZE_RELAYER_URL}/intents/submit`:

```json
{
  "type": "joinProportional",
  "user": "0x…",
  "pool": "0x…",
  "tokensIn": ["0xWETH…", "0xUSDC…"],
  "amountsIn": ["500000000000000000", "1250000000"],
  "minPoolTokensOut": "1234500000000000000",
  "recipient": "0x…",
  "deadline": 1789000000,
  "nonce": 0,
  "signature": "0x…"
}
```

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

## Step 6 — 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`; no on-chain effect.

**LP shares are auto-staked** (deployed behavior, verified live): when
`MultiHopRouter.poolToGaugeVault(pool)` (view → `address`) is non-zero — true for the
listed pools — join shares are staked into the LPGaugeVault for the recipient, so
`ERC20(pool).balanceOf(recipient)` stays 0. The LP position is
`pool.balanceOf(recipient) + gauge.getEligibleBalance(recipient)`.

## What to build

1. A **Ryze proportional-join module** in this repo's conventions:
   - `getProportionalAmounts(pool, primaryToken, primaryAmount)` → all-token amounts at
     current ratio
   - `getProportionalJoinQuote(pool, tokensIn[], slippageBps)` → quote + `minPoolTokensOut`
   - multi-token allowance check / approval helpers
   - `buildJoinProportionalIntent(...)` → unsigned EIP-712 payload
   - `submitJoinProportionalIntent(intent, signature)``intentId`
   - `waitForIntent(intentId, {pollMs, timeoutMs})`
   - convenience `joinProportional(...)` composing the flow
2. **Config** via the env vars above.
3. **Typed errors** (quote failure, missing approvals — listed per token, nonce conflict,
   validation 400s, timeout).
4. **Tests**: mock HTTP with exact shapes above; assert EIP-712 array encoding and field
   order, the `minSharesOut`(signed)/`minPoolTokensOut`(body) mapping, integer math,
   multi-approval detection, polling state machine.

## Acceptance checklist

- [ ] No oracle/price code, no direct contract execution, no raw calldata.
- [ ] `minSharesOut` in the signed message equals body `minPoolTokensOut`, derated for
      `intentFee()` on `tokensIn[0]`.
- [ ] LP position read as wallet + gauge (`poolToGaugeVault`/`getEligibleBalance`).
- [ ] `tokensIn`/`amountsIn` aligned, ≥2, no duplicates, all > 0 — validated before signing.
- [ ] All input tokens' allowances verified before submission.
- [ ] `joinProportionalNonces` used; fresh read; in-flight serialization.
- [ ] 202/400/429 and `pending/confirmed/failed` handled and tested.
- [ ] Everything configurable via env; defaults point at Base Sepolia.