Skip to content

feat(scenarios): repair Raydium AMM v4 and add Raydium CLMM templates - #21

Open
92Infinitus92 wants to merge 11 commits into
feat/scenarios/override/raydium-supportfrom
feat/scenarios/raydium
Open

92Infinitus92 wants to merge 11 commits into
feat/scenarios/override/raydium-supportfrom
feat/scenarios/raydium

Conversation

@92Infinitus92

@92Infinitus92 92Infinitus92 commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Repairs the Raydium AMM v4 templates and adds Raydium CLMM (amm_v3) state prep.

AMM v4 stores no discriminator, so the bundled IDL used to carry made-up ones and no
override ever matched a real pool. The IDL now declares discriminator: [] for AmmInfo
and TargetOrders, and the forge resolver matches such types by exact body size when no
discriminated type claims the account. Data shorter than every discriminator, or with
leftover bytes, fails with the usual "no account type" error. The hard-coded market list
and the script that generated it are gone; templates take a pool address.

Templates:

  • v4: raydium-amm-pool-state, raydium-amm-fees, raydium-amm-swap-stats
  • CLMM: raydium-clmm-pool-state, raydium-clmm-amm-config (fee tier by config index,
    address derived), raydium-clmm-custom

YAML and IDL only, like Kamino: no protocol Rust and no MCP tool. The price/tick coupling,
the tick array PDA recipe and the "stay inside the current tick array" rule live in the
pool-state template's llm_context and in the README; the Studio card computes the shock
client-side (LimeChain/surfpool-web-ui#19).

Tests: resolver unit tests for the exact-fill, same-size ambiguity and longer-discriminator
cases; live tests (--features integration-tests) round-trip real mainnet pools byte for
byte and apply overrides through the materializer, asserting only the targeted bytes
change. Verified on a forked surfnet: a 0.5 shock on the SOL/USDC pool made a real swap
fill at half the mainnet quote, and status 3 on the v4 pool made the program reject swaps.

RetriggerConfidence Score: 4/5

The PR appears safe to merge, with non-blocking CLMM documentation corrections recommended.

Fix All in Claude CodeFindings

  1. P2 CLMM examples omit account fields ▶
  2. P2 Studio card is unavailable ▶
Fix with agent prompt
### Issue 1
crates/core/src/scenarios/protocols/raydium/v3/README.md:80-81
The pool example uses `address`, and the fee-tier example supplies no account address. Neither can be submitted as an `OverrideInstance`: that contract requires `account` in pubkey or PDA form. Show the complete account field in both examples so readers can use them in a scenario.

### Issue 2
crates/core/src/scenarios/protocols/raydium/v3/README.md:118
The price-shock instructions say a Studio Raydium card computes the price, tick, and tick-array values. The current [Studio scenario components](https://github.com/limechain/surfpool-web-ui/blob/HEAD/apps/studio/src/components/svm/scenarios-bento.tsx) provide a PumpSwap price-shock dialog, not a Raydium one. Readers must calculate these values themselves; remove the claim until that card is available, or provide a manual procedure.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

The PR makes Raydium AMM v4 accounts resolvable without invented discriminators, replaces the v4 market-derived template with pool-address templates, and adds CLMM pool and fee-tier templates. The principal follow-ups are to make the CLMM examples valid scenario instances and align the price-shock documentation with the available Studio UI.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  T[Raydium template and instance] --> A[Resolve pool address or config PDA]
  A --> F[Fetch or read local account]
  F --> R[Resolve IDL account type]
  R --> O[Apply field overrides]
  O --> S[Updated local account]
Loading

Reviews (1) · Last reviewed commit: "feat(scenarios): repair Raydium AMM v4 a..."

Comment on lines +80 to +81
"templateId": "raydium-clmm-pool-state",
"address": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CLMM examples omit account fields The pool example uses address, and the fee-tier example supplies no account address. Neither can be submitted as an OverrideInstance: that contract requires account in pubkey or PDA form. Show the complete account field in both examples so readers can use them in a scenario.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/core/src/scenarios/protocols/raydium/v3/README.md
Line: 80-81

Comment:
**CLMM examples omit account fields** The pool example uses `address`, and the fee-tier example supplies no account address. Neither can be submitted as an `OverrideInstance`: that contract requires `account` in pubkey or PDA form. Show the complete account field in both examples so readers can use them in a scenario.

**Knowledge Base Used:**
- [Scenario execution](https://app.greptile.com/limechain/-/custom-context/knowledge-base/limechain/surfpool/-/docs/scenario-execution.md)
- [Shared data contracts](https://app.greptile.com/limechain/-/custom-context/knowledge-base/limechain/surfpool/-/docs/shared-data-contracts.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Cursor

array, `start_index = floor(tick / (tick_spacing * 60)) * tick_spacing * 60`. A move that
stays inside the array covering the current tick is always safe; a larger move needs the
destination array to exist (`getAccountInfo` on the PDA returns null when it does not, and
the swap would fail). The Studio Raydium card computes this for you.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Studio card is unavailable The price-shock instructions say a Studio Raydium card computes the price, tick, and tick-array values. The current Studio scenario components provide a PumpSwap price-shock dialog, not a Raydium one. Readers must calculate these values themselves; remove the claim until that card is available, or provide a manual procedure.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/core/src/scenarios/protocols/raydium/v3/README.md
Line: 118

Comment:
**Studio card is unavailable** The price-shock instructions say a Studio Raydium card computes the price, tick, and tick-array values. The current [Studio scenario components](https://github.com/limechain/surfpool-web-ui/blob/HEAD/apps/studio/src/components/svm/scenarios-bento.tsx) provide a PumpSwap price-shock dialog, not a Raydium one. Readers must calculate these values themselves; remove the claim until that card is available, or provide a manual procedure.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex Fix in Cursor

@github-actions

Copy link
Copy Markdown
  • P2 — Price shocks can leave incorrect active liquidity. overrides.yaml:206 says moving within the current tick array is “always safe.” Such a move can cross initialized ticks whose liquidity_net changes active liquidity. Changing only price/tick leaves stale liquidity, producing incorrect quotes or failed swaps. Restrict shocks to the current interval between initialized ticks, or recompute liquidity across crossed ticks; update the README too.

  • P2 — CLMM examples are not valid override instances. README.md:80 uses address instead of account, and the fee-config example omits account. Both also omit required id, enabled, and scenarioRelativeSlot. Provide complete examples using account: {"pubkey": "..."} or the PDA representation.

Tests could not run: the pinned toolchain required a prohibited filesystem write; the installed toolchain lacked cached dependencies for offline execution.

@github-actions

Copy link
Copy Markdown
  • P2 — Incomplete liquidity guidance: v3/overrides.yaml:129 tells raydium-clmm-custom callers to update price and tick but omits the liquidity adjustment added to raydium-clmm-pool-state. Crossing initialized ticks while following these instructions leaves incorrect active liquidity and swap amounts. Include the same liquidity rule here and apply it to the README’s worked example.

  • P2 — CLMM examples cannot deserialize: v3/README.md:80 uses address instead of account; the fee-tier example omits account entirely. Both also lack required id, scenarioRelativeSlot, and enabled fields. Provide complete OverrideInstance examples, including the config’s pubkey or PDA.

  • P2 — Review workflow fails for fork PRs: openai-review.yml:37 requires OPENAI_API_KEY, which GitHub withholds from fork-triggered pull_request workflows. Guard unsupported runs or provide a trusted review mechanism; otherwise external contributions receive a failed check without feedback.

Tests could not run: Rustup attempted to write to a read-only directory while initializing the pinned toolchain.

@github-actions

Copy link
Copy Markdown
  • P2 — Fork PRs fail the new review workflow (openai-review.yml:38). GitHub withholds repository secrets on forked pull_request runs, so OPENAI_API_KEY is unavailable. Skip the review job for forks or when credentials are absent.

  • Documentation improvement: The CLMM recipes still aren’t usable scenario payloads: they omit account and use template instead of templateId, with fields outside values. Label them as shorthand and include one complete OverrideInstance example.

No additional concrete bugs found in the resolver/template changes. Tests could not run because Rustup attempted to write to a read-only directory.

@92Infinitus92
92Infinitus92 changed the base branch from develop to feat/scenarios/override/raydium-support October 11, 2026 05:46
@92Infinitus92
92Infinitus92 force-pushed the feat/scenarios/raydium branch from a9eba96 to 691c199 Compare October 11, 2026 05:46

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR fixes the Raydium AMM v4 templates. The bundled IDL now declares discriminator: [], and the resolver matches undiscriminated types by exact size. It also adds CLMM templates (pool-state, amm-config, custom), adds a collection-level llm_context, and expands the call_surfnet_rpc param docs.

The core change in resolve_idl_account_for_data looks correct:

  • The longest discriminator wins.
  • Undiscriminated types are tried only when no discriminated type matches, and they must fill the account exactly.
  • Same-size undiscriminated types are rejected as ambiguous.
  • Short data no longer panics (the old encoding path sliced data[..8] without a length check).

I checked the documented offsets against the program layouts and they are right:

  • CLMM: 73/105/137, amm_config @9, status bits.
  • v4: 336/368/400/432, fees @128.
  • CPMM: 168/200/72, amm_config @8, dataSize 637.

The status enums and bitmasks also match the programs.

Scope and description

  • The diff also adds Raydium CPMM templates (raydium-cpmm-pool-state, raydium-cpmm-amm-config, a new IDL and live tests). It also swaps CI to a Claude review workflow (.github/workflows/claude-review.yml) and changes the MCP call_surfnet_rpc schema text. The PR title and body mention none of these. Please update the description, or split them out, so reviewers know they are included.
  • In the review workflow, the PR body is interpolated straight into the prompt. Fork PRs are skipped and tools are read-only, so the blast radius is small, but it is a prompt-injection surface worth being aware of.

Minor

  • In mixed IDLs, a discriminated type that matches by prefix but fails to decode stops the undiscriminated fallback from running. No bundled IDL mixes the two kinds today, and the new test enforces that, so this is only worth a comment in the code.
  • The undiscriminated path decodes the account body twice: once in the resolver, then again in get_forged_idl_type_data.

Overall the change is solid. The comments below are mostly about the docs and LLM guidance leaning on a Studio card that may not exist yet.

PRICE: sqrt_price_x64 = sqrt(price_raw) x 2^64, where price_raw is token_1 per token_0 in raw units,
and tick_current = floor(log(price_raw) / log(1.0001)); set both together. A price move must also
set liquidity to the active liquidity at the new tick, and the tick array covering the new tick must
exist. The Studio price shock computes all three; prefer it to hand-computed values.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This collection-level llm_context reaches MCP and LLM clients, which can't use the Studio price shock. The card is also still in an open PR (surfpool-web-ui#19). Telling the model to "prefer it to hand-computed values" gives it nowhere to go, so it will likely set sqrt_price_x64/tick_current and skip liquidity.

Suggest replacing this with the manual procedure the README already documents:

  • Cross ticks and add or subtract liquidity_net.
  • Keep the move within the current tick array, otherwise refuse it.

The same applies to the CPMM collection context ("the Studio price shock writes both vaults").


## Shock the price

Use **Scenario presets → Raydium state → CLMM price shock** in Studio. It reads the pool and the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This recipe points readers to Scenario presets → Raydium state → CLMM price shock, but that depends on surfpool-web-ui#19, which hasn't landed. Please either gate the wording ("once available") or spell out the manual steps first: read the tick arrays, sum liquidity_net across the crossed ticks, and check that the destination array PDA exists. The troubleshooting row at line 90 has the same dependency.

.into_iter()
.filter(|account| data.starts_with(&account.discriminator))
.collect();
if candidates.is_empty() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The undiscriminated fallback runs only when no discriminated type matches by prefix. In a mixed IDL, a discriminated type can match by prefix and then fail to decode. When that happens, the matching undiscriminated type is never tried, and the forge fails with a decode error instead of resolving.

No bundled IDL mixes the two today, and the registry test enforces that. Still, please add a short comment stating the assumption, or only drop the undiscriminated candidates when a discriminated one actually decodes.

))
})?;
// Find the account type, then split its discriminator off the data
let account_def = resolve_idl_account_for_data(idl, account_data).map_err(|reason| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For undiscriminated accounts, the resolver already decoded the whole body to check that it fills the account exactly. get_forged_idl_type_data then decodes it again. That's cheap at 752 bytes, but TargetOrders is ~2.2 KB with nested arrays. Consider having the resolver return the parsed Value so the forge path can reuse it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant