Skip to content

feat(scenarios): add Orca Whirlpool templates - #22

Open
92Infinitus92 wants to merge 8 commits into
feat/scenarios/override/whirlpool-supportfrom
feat/scenarios/whirlpool
Open

92Infinitus92 wants to merge 8 commits into
feat/scenarios/override/whirlpool-supportfrom
feat/scenarios/whirlpool

Conversation

@92Infinitus92

@92Infinitus92 92Infinitus92 commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Orca Whirlpool state prep.

Templates: whirlpool-pool-state (sqrt price, tick, liquidity, fee rate, protocol fee)
and whirlpools-config (the mainnet singleton). Templates take a pool address; the
README explains how to find one.

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

One caveat, documented in the README and in the template's llm_context: Whirlpool
rejects swaps with InvalidTimestamp while the surfnet clock is behind the pool's last
update, which happens after Studio's Play pauses the clock. surfnet_resumeClock or
surfnet_timeTravel fixes it.

Tests: live tests round-trip a real mainnet pool byte for byte, check the config singleton
against a live pool, and apply overrides through the materializer asserting only the
targeted bytes change. Verified on a forked surfnet: a 0.5 shock made a real swap fill at
half the mainnet quote, and a 1% fee override charged exactly 1%.

@github-actions

Copy link
Copy Markdown
  • [P2] Tick-array bounds do not guarantee a safe price shock. overrides.yaml:54 says moving within the current array is “always safe.” Crossing initialized ticks within that array can change active liquidity. Updating only price/tick leaves stale liquidity, producing incorrect quotes or liquidity-underflow failures on subsequent crossings. Restrict this guidance to moves that cross no initialized ticks, or describe the required liquidity adjustments; update the README too.

  • [P2] Preserve compatibility with existing template IDs. overrides.yaml:12 replaces all six existing Whirlpool IDs without aliases or migration. Saved scenarios and MCP requests referencing IDs such as whirlpool-custom or whirlpool-sol-usdc can no longer resolve them. Keep deprecated aliases or migrate existing references.

Tests were not run.

@github-actions

Copy link
Copy Markdown
  • P2 — Preserve existing template IDs. overrides.yaml:12 replaces all six existing IDs without aliases or migration. Clients using IDs such as whirlpool-custom now fail MCP template validation. Retain deprecated aliases or migrate existing references.

  • P2 — Fix the worked price-shock example. README.md:95 changes price and tick while retaining live liquidity. When the move crosses initialized ticks, this produces incorrect swap amounts—the failure described later in the same README. Include the liquidity calculation in the example or clearly mark the JSON as incomplete.

Tests could not run: Rustup attempted to write to a read-only directory.

@github-actions

Copy link
Copy Markdown
  • P2 — Preserve existing template IDs or migrate them. overrides.yaml:12 replaces all six existing Whirlpool IDs. Clients using IDs such as whirlpool-custom or whirlpool-sol-usdc now receive Unknown templateId from MCP template lookup. Keep deprecated aliases or provide a migration.

  • P2 — Handle forked PRs in the review workflow. openai-review.yml:38 requires OPENAI_API_KEY, but GitHub withholds repository secrets from fork-triggered pull_request workflows. Those runs will fail before producing feedback. Add a guard to skip unsupported runs or use a separately authorized review workflow.

Review based on static inspection; live RPC tests were not run.

@92Infinitus92
92Infinitus92 force-pushed the feat/scenarios/whirlpool branch from fcbf4b2 to bcfbf46 Compare October 8, 2026 05:41
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

This PR swaps the six hardcoded Whirlpool templates for two: a pool template that takes an address (whirlpool-pool-state) and the config singleton (whirlpools-config). It moves the IDL to v1/, adds a README, and adds live mainnet tests. The Whirlpool account offsets used in the tests are correct (fee_rate @45, sqrt_price @65, config @8). The price/tick math, the tick array start-index formula and the liquidity-crossing rules in llm_context and the README match the Whirlpool program.

Main concern: the Kamino liquidation-arbitrage example and the Kamino README no longer do what they describe. The old whirlpool-popcat-sol template pointed at Czfq3…, which is actually a SOL/USDC pool with tick spacing 4, not POPCAT/SOL. The PR fixes the label, but that means the scenario now deepens two SOL/USDC pools. The POPCAT → SOL leg the description promises (seize POPCAT, sell POPCAT → SOL → USDC) is not deepened at all. The example should point at a real POPCAT/SOL Whirlpool, or its description and README should be changed to match.

Smaller points:

  • Removing the six old template IDs is a breaking change for any saved scenario that uses them (e.g. whirlpool-sol-usdc, whirlpool-custom). Consider keeping aliases, or mention it in the changelog.
  • .github/workflows/claude-review.yml appears in the diff but has nothing to do with the feature. Make sure it's meant to be in this PR.

crates/core/src/scenarios/examples/kamino-liquidation-arbitrage.json:40

Now that Czfq3… is correctly labelled SOL/USDC (ts4), this scenario deepens two SOL/USDC pools and no POPCAT pool. The description on line 4 says the exit is POPCAT → SOL → USDC, so the leg that sells the seized collateral still slips. Point this override at an actual POPCAT/SOL Whirlpool, or rewrite the description and the Kamino README recipe.


crates/core/src/scenarios/examples/kamino-liquidation-arbitrage.json:62

50000000000000000 is above 2^53, so any JS client (Studio) parsing this JSON number loses precision. The new Whirlpool README says liquidity is a decimal string, and the PR's own test passes sqrt_price as a string. Use "50000000000000000" here, and on line 48 too for consistency.


crates/core/src/scenarios/protocols/kamino/README.md:84

The comment says "sell the seized collateral", but this pool is SOL/USDC. The seized collateral is POPCAT, so this pool can't be the exit for it. The recipe needs a POPCAT/SOL pool, or the text needs updating.


crates/core/src/scenarios/protocols/whirlpool/v1/overrides.yaml:102

Nit: pools only store and read whirlpools_config, they never write it. Say "stores it as the field at offset 8" so the LLM doesn't conclude pools mutate it.


crates/core/src/tests/whirlpool/mod.rs:119

Nit: wrapping_add(500) can produce a value above the program's max of 60000 (or wrap around). The byte-diff assertion still passes, but a fee rate that stays in range, e.g. if fee < 59500 { fee + 500 } else { fee - 500 }, keeps the test realistic in case validation is added later.

@92Infinitus92
92Infinitus92 changed the base branch from develop to feat/scenarios/override/whirlpool-support October 8, 2026 08:21

@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 replaces the six hard-coded Whirlpool templates with two: whirlpool-pool-state, which takes a pool address, and whirlpools-config, the mainnet singleton. It moves the IDL to whirlpool/v1/, adds labels, descriptions and llm_context, a README, registry unit tests, and live integration tests (byte-for-byte round-trip, config singleton check, materializer byte-diff). The template design is clean, and the coupling, liquidity and clock caveats are well documented.

Issues

  • The Kamino arbitrage example no longer matches its own description. Czfq3x… used to be labelled POPCAT/SOL; this PR relabels it SOL/USDC (tick spacing 4). The scenario description still says the seized collateral is sold POPCAT → SOL → USDC. If Czfq3x… really is SOL/USDC, no POPCAT pool is deepened, and both depth overrides now target SOL/USDC pools.
  • Breaking change: the template IDs whirlpool-sol-usdc, -sol-usdt, -msol-sol, -orca-usdc, -popcat-sol and -custom are removed with no alias. Saved scenarios that use them stop resolving. This should at least be called out in the release notes.
  • Unrelated CI change: the diff adds .github/workflows/claude-review.yml, from commits eb2619a and 7092c18. That is out of scope for a scenarios PR and should go in its own PR.
  • Minor: the integration tests default to the public api.mainnet-beta.solana.com endpoint, which is rate-limited and will be flaky in CI. Consider skipping them unless SURFPOOL_TEST_RPC_URL is set.

```
whirlpool-popcat-sol liquidity: 5000000000000000 # sell the seized collateral
whirlpool-sol-usdc liquidity: 50000000000000000 # route back to the debt token
whirlpool-pool-state on Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE (SOL/USDC, tick spacing 4)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Czfq3x… was previously the whirlpool-popcat-sol template, used to "sell the seized collateral" (POPCAT). It is now labelled SOL/USDC tick spacing 4. If that label is right, this recipe and examples/kamino-liquidation-arbitrage.json no longer deepen any POPCAT pool. The example's description still says the route is POPCAT → SOL → USDC. Either point this at an actual POPCAT/SOL Whirlpool, or update the example's description and route.

together:
price_raw = (sqrt_price / 2^64)^2
price = price_raw * 10^(decimals_a - decimals_b)
tick_current_index = floor(log(price_raw) / log(1.0001))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Floating-point floor(log(...)/log(1.0001)) can be off by one when the price sits near a tick boundary. A mismatched tick_current_index is exactly the failure described a few lines below. Consider telling the LLM to check that sqrt_price_at(tick) <= sqrt_price < sqrt_price_at(tick+1), or to nudge sqrt_price slightly inside the tick.


This template's address is the one deployed WhirlpoolsConfig singleton,
2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ. Every Whirlpool pool was created under it and
reads/writes it as the field at offset 8 of its own Whirlpool account.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: pools only read whirlpools_config, they never write it. "reads/writes" could mislead the model into thinking pool state changes propagate to the config.

@github-actions

Copy link
Copy Markdown

Summary

This PR replaces the six hardcoded Whirlpool templates (whirlpool-sol-usdc, -popcat-sol, -custom, and so on) with two:

  • whirlpool-pool-state, which takes any pool address.
  • whirlpools-config, which points at the mainnet WhirlpoolsConfig singleton.

It also moves the IDL to v1/, adds a how-to README and detailed llm_context, updates the Kamino arbitrage example and README, and adds live mainnet integration tests (round-trip, config singleton, materializer byte-diff).

I checked the hardcoded offsets against the Whirlpool layout and they are right: token_mint_a@101, token_vault_a@133, token_mint_b@181, size 653, fee_rate@45, sqrt_price@65, config@8. The coupling and liquidity-crossing rules (old < t <= new add / new < t <= old subtract) are also correct. The tests are well targeted.

Issues

  • Kamino arbitrage example no longer matches its story. Czfq3… used to be labelled POPCAT/SOL; this PR correctly relabels it SOL/USDC (tick spacing 4). But now both deepened pools are SOL/USDC, while the scenario description and the Kamino README still say the seized POPCAT is sold "POPCAT → SOL → USDC" / "sell the seized collateral". Nothing deepens a POPCAT/SOL pool, so the exit leg the example promises is not prepared.
  • Breaking change for saved scenarios. Removing whirlpool-sol-usdc, whirlpool-custom and the other old IDs breaks any user scenario that references them, with no alias or migration. Call this out in release notes or keep deprecated aliases for a release.
  • Minor: the directory is now v1/, but version: v0.7.0 is unchanged in the YAML. That's fine if intentional, but the README index calls it "Whirlpool v1".
  • Minor: the integration tests default to the public api.mainnet-beta.solana.com. They're gated behind integration-tests, but expect occasional rate-limit flakes in CI unless SURFPOOL_TEST_RPC_URL is set.

crates/core/src/scenarios/examples/kamino-liquidation-arbitrage.json:4

Both deepened pools are now SOL/USDC (Czfq3… tick spacing 4 and HJPj… tick spacing 64), but the description still says the seized collateral is sold "POPCAT -> SOL -> USDC". Nothing deepens the POPCAT -> SOL leg anymore. Either add the real POPCAT/SOL pool address, or rewrite the description and the first override's label ("so the exit does not slip") to match what the scenario actually does.


crates/core/src/scenarios/protocols/kamino/README.md:84

Same mismatch as the example JSON: this pool is SOL/USDC, so the comment # sell the seized collateral (POPCAT) no longer describes it. With both lines on SOL/USDC pools, the recipe never covers the collateral → SOL hop.


crates/core/src/scenarios/examples/kamino-liquidation-arbitrage.json:62

Nit: the Whirlpool README says u128 values like liquidity should be passed as decimal strings, but this example uses JSON numbers. 50000000000000000 happens to be exactly representable as a double, but a string ("50000000000000000") matches the documented convention and avoids precision loss for anyone who copies this into JS tooling with a different value.


crates/core/src/scenarios/protocols/whirlpool/v1/overrides.yaml:69

floor(log(price_raw)/log(1.0001)) in floating point can be off by one when the price lands on or very near a tick boundary. Whirlpool also sets tick_current_index = t - 1 when a downward swap ends exactly at tick t's sqrt price. Consider telling the model to check that sqrt_price_at(tick) <= sqrt_price < sqrt_price_at(tick+1) and adjust by ±1. Otherwise it can produce an inconsistent pair that the README's troubleshooting table says makes the swap fail.


crates/core/src/tests/whirlpool/mod.rs:119

Nit: wrapping_add(500) can produce a value above the program max of 60000, or wrap to a small number. That doesn't matter for the byte-diff assertion, but checked_add(500).filter(|v| *v <= 60_000) or a fixed value like 10_000 keeps the test inside valid state, in case someone later extends it to run a swap.

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