Skip to content

fix(curve): derive unpriced legs with get_dy - #41

Open
matheus1lva wants to merge 4 commits into
mainfrom
matheus1lva/curve-get-dy-oracle
Open

fix(curve): derive unpriced legs with get_dy#41
matheus1lva wants to merge 4 commits into
mainfrom
matheus1lva/curve-get-dy-oracle

Conversation

@matheus1lva

@matheus1lva matheus1lva commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Curve LP reserve NAV now prices pools where one or more constituent coins have no market price. The adapter resolves the child prices it can, quotes one unit of each missing coin against the largest priced reserve with the pool's get_dy, and keeps the existing NAV calculation when every leg is priced. Unsafe quotes (revert, zero, non-finite) return no price.

A child price that fails transiently is not the same as one that does not exist. optionalChildren rethrows retryable and budget child failures as a RecursiveDependencyError, so a flaky RPC surfaces as UNAVAILABLE instead of quietly switching the pool to a derived valuation. Only genuinely unpriceable coins (unsupported, invalid, cycle, max-depth) feed the derivation path.

The anchor carries the only market price behind every derived leg, so a pool whose anchor holds less than 1% of pool USD value (MIN_ANCHOR_SHARE) gets no price at all. The share is measured on value, not on summed token amounts, which do not compare across coins that trade at different prices.

Metadata records each derived coin, its priced anchor, the one-unit input, and the raw quote, under valuationRule: 'get-dy-derived-constituents'.

Test plan

  • bun install
  • bun run typecheck
  • bun run lint
  • npx vitest run --exclude test/enso.integration.test.ts (239 passed)
  • npx vitest run (the two live Enso integration tests receive HTTP 401)
  • New unit tests: multi-leg derivation, largest-reserve anchor selection across two priced coins, sub-1% value anchor rejected, transient constituent failure resolving as retryable

Two historical spot checks were run against the first commit, before the anchor guard moved from amount share to value share. The derivation math they exercise is unchanged, but neither has been re-run since.

  • Historical Ethereum hCRV check: $59,167.95 at timestamp 1617321599
  • Historical Fantom dust-anchor check: no price returned instead of publishing an invalid derived value

@matheus1lva
matheus1lva marked this pull request as ready for review August 23, 2026 23:10
optionalChildren rethrows retryable/budget child failures instead of
reporting them as unpriceable, so a flaky RPC no longer switches the pool
to a get_dy-derived valuation.

Dust-anchor guard now measures the anchor's share of pool USD value, not
its share of summed token amounts. Derivation moved to deriveMissingLegs.
@matheus1lva

Copy link
Copy Markdown
Collaborator Author

/review

@github-actions

Copy link
Copy Markdown

Review started (review-pr): https://github.com/yearn/yearn-prices/actions/runs/32988773165

@github-actions

Copy link
Copy Markdown

Summary

PR #41 extends the Curve reserve-NAV adapter so pools with one or more constituents lacking a market price can still be valued. Previously requireChildren demanded every coin resolve to a market price or the pool went unpriced; now optionalChildren lets individual legs come back null, and deriveMissingLegs fills them in by quoting get_dy against the most valuable priced "anchor" reserve. A MIN_ANCHOR_SHARE (1%) floor rejects derivations where the anchor is too thin a slice of pool value to trust, and transient/budget RPC failures on a constituent are rethrown (not swallowed into a derived guess) via RecursiveDependencyError. Test coverage in curve.test.ts/helpers.ts is thorough and each scenario's arithmetic checks out (anchor-by-value selection, multi-leg derivation, dust-anchor rejection, retryable propagation).

gitconfig-mask: sentinel

Dependencies

No package.json changes in this diff — not applicable.

Verification

  • npm run typecheck, npm run lint, npm run testnot runnable in this CI sandbox (no network access; node_modules incomplete — biome/vitest binaries missing, @cloudflare/workers-types types absent). Reviewed by reading source and hand-verifying the new tests' arithmetic instead.
  • npm-policy size/maintenance lookups: not applicable (no new deps).

Issues

None blocking.

Suggestions

  • src/sources/onchain/adapters/curve.ts:157-181 (readGetDy) — for each derived leg, this always probes the uint256 get_dy ABI before falling back to int128, even though the pool's index-argument type is already known from readCoinAddress (coin.indexType, used at curve.ts:385 for balances). Threading indexType through CurveCoin and into readGetDy would cut the extra revert-driven RPC call per derived leg on int128-style pools, which matters given the adapter's own ReadBudgetExceededError budget tracking. Not correctness-affecting since maybe() handles the fallback safely — pure efficiency.
  • src/sources/onchain/adapters/curve.ts (deriveMissingLegs) — worth a comment/README note (or a follow-up ticket) that get_dy-derived legs read spot AMM pricing at a single block, which is more directly manipulable by same-block reserve-skewing (e.g. a large swap just before the priced block) than the balance-share NAV math it's layered onto. The MIN_ANCHOR_SHARE guard protects against a thin anchor but not against a well-funded anchor whose ratio was pushed around in-block. May already be an accepted tradeoff elsewhere in the pricing pipeline (e.g. downstream sanity/deviation checks not visible in this diff) — flagging for confirmation rather than as a blocker.

Verdict

COMMENT — implementation and tests are sound; the two notes above are non-blocking efficiency/hardening suggestions for the author to weigh.


How This Was Reviewed

This review was conducted using the review-pr skill, diffed against b4445a73d4e5c8cf77fff12043824c9b0024731d, in a non-interactive CI environment without network/browser access (no GitHub API, no PR post, no visual verification).

@matheus1lva

Copy link
Copy Markdown
Collaborator Author

/review-workflow

1 similar comment
@matheus1lva

Copy link
Copy Markdown
Collaborator Author

/review-workflow

@github-actions

Copy link
Copy Markdown

Review started (review-pr-workflow): https://github.com/yearn/yearn-prices/actions/runs/32990186700

@github-actions

Copy link
Copy Markdown

Review started (review-pr-workflow): https://github.com/yearn/yearn-prices/actions/runs/32991123354

@github-actions

Copy link
Copy Markdown

Summary

Curve pool pricer now derives prices for constituents the market can't price directly, by quoting each against the most valuable priced reserve (get_dy), instead of requiring every leg to have a market price. Transient/budget-capped child lookups now fail retryably rather than silently falling back, and a derived pool is only priced if the anchor coin holds ≥1% of pool value. Tests and helpers updated accordingly. (PR metadata not fetchable — no network access in this CI run; reconstructed from commit messages a268a51, 3c41364, 8db3c50.)

tier: light — 4 files, ~147 net source lines, single top-level directory (src/sources/onchain), no auth/token/CI/dependency-change triggers. Lint/tests not run: no network access to install dependencies in this CI sandbox — not verifiable in CI.

Issues

None found.

Suggestions

None.

Verdict

APPROVE


How This Was Reviewed

Reviewed with the review-pr-workflow skill — 2 review lenses (spec-conformance, bugs), each finding independently verified by claude. 0 candidate findings were refuted and dropped.

gitconfig-mask: sentinel

@matheus1lva

matheus1lva commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@murderteeth 0 issues, ready.

@murderteeth murderteeth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Curve reserve-NAV now prices pools with unpriceable constituents by quoting each missing coin against the largest priced reserve via get_dy, and correctly distinguishes a transient child failure from an absent price. The transient/budget rethrow in optionalChildren is the right call and is well tested. Lint clean in changed files, tsc clean, 240 tests pass.

Issues

  • src/sources/onchain/adapters/curve.ts:242 - Dust-anchor guard never fires (high) — the 1% floor is meant to refuse a pool whose priced reserve is a rounding error, but it measures the anchor's share using prices derived from that same anchor, so draining the anchor raises the computed share instead of lowering it. I simulated a two-coin stableswap (A=100, 0.04% fee) holding 1,000,000 unpriced units: as the anchor falls from 1,000,000 units to 1 unit the computed share never drops below 6.8% and ends at 32%, so the guard passes every step, while the NAV it publishes falls from ~$2.0M to ~$3.10. The existing dust test only passes because its fixture quotes 1,000,000 anchor units per coin — the guard detects over-quoting, not depletion.
    • Done when: reducing the anchor reserve across a series of otherwise-identical pools never makes the accept/reject decision more permissive, and a pool holding 1,000,000 unpriced units against 5,000 $1 anchor units quoting get_dy ≈ 0.036 returns no price; a test pins the series.
    • Provenance: 3c41364
  • src/sources/onchain/adapters/curve.ts:225 - Derived NAV tracks pool imbalance (high) — a derived leg marks the pool's entire reserve of that coin at the marginal one-unit swap rate, so the published LP price follows how skewed the pool is at that block rather than any outside reference. In the same simulation the price moved 6.8× while the anchor still held 2% of its starting balance, with nothing rejecting it. Exposure scales with the derived legs' share of pool value, so a single small unpriced leg is barely affected.
    • Done when: a pool published under valuationRule: 'get-dy-derived-constituents' has a stated bound on how far its price may move as the reserve ratio changes, and a test sweeps one fake pool from balanced to heavily imbalanced and asserts the price stays inside that bound — or that no price is published for such pools.
    • Provenance: 3c41364

Verdict

REQUEST_CHANGES


How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses, each finding independently verified by claude. 5 candidate findings were refuted and dropped.

one-unit get_dy says nothing about depth: a drained or skewed pool marks
its whole unpriced reserve at a marginal rate it cannot settle, and the
anchor-share guard measured that share with prices derived from the same
anchor, so draining the anchor made it more permissive.

each derived leg now also quotes get_dy for its entire balance and is
refused unless the pool pays out at least half the marked value. that is
monotone in anchor depletion and bounds published NAV at ~3x the anchor's
market value. threshold 0.5 is generous for the pegged/stable pools this
targets; a balanced constant-product pool sits at the knife edge.

tests: anchor-drain series never flips back to accept, 1M unpriced vs
5,000 anchor at 0.036 returns no price. fake get_dy is now dx-aware.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants