Skip to content

🧹 Add USD pricing for Drift perp markets without spot mints#75

Closed
zknpr wants to merge 1 commit intomainfrom
fix-drift-perp-pricing-7359130521910270343
Closed

🧹 Add USD pricing for Drift perp markets without spot mints#75
zknpr wants to merge 1 commit intomainfrom
fix-drift-perp-pricing-7359130521910270343

Conversation

@zknpr
Copy link
Copy Markdown
Owner

@zknpr zknpr commented Mar 8, 2026

🎯 What: The code health issue addressed
Adds logic to fetch the oracle price and calculate USD value for Drift perp positions that lack a corresponding spot mint (e.g., HYPE-PERP).

💡 Why: How this improves maintainability
Previously these positions were skipped, leaving a TODO comment. By reading the oracle price from the market account buffer (which is already prefetched), we correctly attribute this value to USDC without adding complex external calls.

Verification: How you confirmed the change is safe
Ran node test.js projects/neutral-trade to verify that the HYPE-PERP and other mint-less positions are correctly priced and that the TVL accumulates accurately, and that there are no runtime crashes or mixups.

Result: The improvement achieved
Eliminated the "skipped base leg" workaround, completing the original author's TODO.


PR created automatically by Jules for task 7359130521910270343 started by @zknpr

…ints.

Instead of ignoring these perp positions, the value is calculated using
the oracle price from the perp market account data and attributed to
the base quote token (USDC).

Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

Warning

Rate limit exceeded

@zknpr has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b15e37fa-d8de-4266-8cc0-b6a78df2b025

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3be2a and 4d12bb6.

📒 Files selected for processing (1)
  • projects/neutral-trade/utils/drift.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-drift-perp-pricing-7359130521910270343

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@llamabutler
Copy link
Copy Markdown

The adapter at projects/neutral-trade exports TVL:

solana                    49.01 M

total                    49.01 M 

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR completes a long-standing TODO by pricing perp positions that lack a corresponding Solana spot mint (e.g. HYPE-PERP) using the cached oracle price stored in the Drift PerpMarket account buffer rather than skipping them. The arithmetic — (base_asset_amount × last_oracle_price) / 10^9 — is consistent with Drift's BASE_PRECISION (1e9) and PRICE_PRECISION (1e6), producing a result in USDC token units (1e6) that is then added alongside the existing quoteBalance, correctly capturing unrealized PnL for mint-less perp positions.

Key points:

  • getPerpMarketOraclePrice reads offset 72 in the PerpMarket buffer, which is amm.historical_oracle_data.last_oracle_price — a cached value written the last time any instruction touched the market, not a live Pyth/Switchboard oracle read. For active markets like HYPE-PERP this is fine in practice, but the offset should be documented as a named constant.
  • There is no validation that the returned oracle price is positive (> 0n). An uninitialised or malformed market account would produce oraclePrice = 0n or a negative value, silently zeroing out or inverting the USD contribution without throwing.
  • No extra RPC calls are introduced; the perpAccountMap buffers are already fetched earlier in getTvl, keeping the change efficient.

Confidence Score: 3/5

  • Safe to merge with minor fixes — the math is correct but a missing guard on the oracle price could silently corrupt TVL for edge-case market states.
  • The calculation logic is mathematically sound and consistent with Drift's precision conventions. However, the absence of a price > 0n check means an uninitialised or invalid market account would produce a silent wrong result rather than a loud error, which is a meaningful correctness risk in a TVL adapter. The stale-price concern is low-impact for active markets but the undocumented magic offset adds maintenance risk.
  • projects/neutral-trade/utils/drift.js — specifically the new getPerpMarketOraclePrice function and its call site

Important Files Changed

Filename Overview
projects/neutral-trade/utils/drift.js Adds getPerpMarketOraclePrice (reads amm.historical_oracle_data.last_oracle_price at offset 72) and uses it to attribute USD value for perp positions that lack a spot mint (e.g. HYPE-PERP). The arithmetic is correct (BASE_PRECISION 1e9 / PRICE_PRECISION 1e6 → USDC 1e6), but the function uses a magic number offset without documentation, and there is no guard preventing a zero or negative oracle price from silently producing an incorrect TVL value.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getTvl: iterate perpPositions] --> B{meta.mint exists?}
    B -- Yes --> C[api.add baseTokenMint, baseBalance]
    B -- No\ne.g. HYPE-PERP --> D[getPerpMarketOraclePrice\nread PerpMarket buffer at offset 72\namm.historical_oracle_data.last_oracle_price]
    D --> E{oraclePrice valid?\nNO GUARD - NEW RISK}
    E -- price > 0 --> F["usdValue = base_asset_amount × oraclePrice / 10^9"]
    E -- price ≤ 0 --> G[Silent wrong/zero TVL]
    F --> H[api.add USDC mint, usdValue]
    C --> I[api.add USDC mint, quoteBalance]
    H --> I
Loading

Last reviewed commit: 4d12bb6

if (!accountInfo) {
throw new Error(`No account info found for market`);
}
return accountInfo.data.readBigInt64LE(72);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undocumented magic offset — stale cached price

The hardcoded offset 72 corresponds to amm.historical_oracle_data.last_oracle_price in the Drift PerpMarket account layout:

[0–7]   discriminator    (8 bytes)
[8–39]  pubkey           (32 bytes)
[40–71] amm.oracle       (32 bytes, the oracle Pubkey)
[72–79] amm.historical_oracle_data.last_oracle_price  ← this field

Two things worth noting:

  1. This is a cached/stale price stored in the market account, updated the last time any instruction touched the market (trade, liquidation, crank). For frequently traded markets like HYPE-PERP this is fine, but for less active perps it could lag the true market price. The value is not a live Pyth/Switchboard oracle read.

  2. The bare 72 gives no indication of its meaning to future readers. Consider extracting it to a named constant:

Suggested change
return accountInfo.data.readBigInt64LE(72);
const ORACLE_PRICE_OFFSET = 72; // amm.historical_oracle_data.last_oracle_price (i64)
return accountInfo.data.readBigInt64LE(ORACLE_PRICE_OFFSET);

Comment on lines +1931 to +1936
function getPerpMarketOraclePrice(accountInfo) {
if (!accountInfo) {
throw new Error(`No account info found for market`);
}
return accountInfo.data.readBigInt64LE(72);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No guard against a zero or negative oracle price

readBigInt64LE returns a signed 64-bit BigInt. If the perp market account is uninitialised, freshly created, or has an invalid state, last_oracle_price could be 0n or negative. Multiplying base_asset_amount by such a value and dividing will silently produce a zero or wrong-sign USD contribution, corrupting the TVL without any visible error.

The existing getPerpMarketFundingRates function has the same absence of value validation, but that computation is additive PnL and the error is bounded. Here the entire notional USD value of a base-asset leg depends on this single field.

Consider adding a guard before returning:

Suggested change
function getPerpMarketOraclePrice(accountInfo) {
if (!accountInfo) {
throw new Error(`No account info found for market`);
}
return accountInfo.data.readBigInt64LE(72);
}
function getPerpMarketOraclePrice(accountInfo) {
if (!accountInfo) {
throw new Error(`No account info found for market`);
}
const price = accountInfo.data.readBigInt64LE(72);
if (price <= 0n) {
throw new Error(`Invalid oracle price ${price} read from market account`);
}
return price;
}

@zknpr
Copy link
Copy Markdown
Owner Author

zknpr commented Mar 8, 2026

Closing: duplicate of PR #73 (drift base leg pricing). #73 has better precision documentation and clearer variable naming.

@zknpr zknpr closed this Mar 8, 2026
@google-labs-jules
Copy link
Copy Markdown

Closing: duplicate of PR #73 (drift base leg pricing). #73 has better precision documentation and clearer variable naming.

Understood. Acknowledging that this work is a duplicate and stopping work on this task.

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.

2 participants