Intermediate token amount differs between reference quote and exact signed transaction simulation in an atomic DLMM → PumpSwap swap
Intermediate token amount differs between reference quote and exact signed transaction simulation in an atomic DLMM → PumpSwap swap
Loading saved threads...
Aniello Ambrosio · External communityPost link
External question — Solana Stack Exchange
Author: Aniello Ambrosio
Original post: https://solana.stackexchange.com/questions/24508
License: CC BY-SA 4.0 — https://creativecommons.org/licenses/by-sa/4.0/
Adaptation: HTML converted to plain text; contact email addresses removed.
I am debugging an atomic two-leg DEX arbitrage transaction on Solana and I have reached a deterministic discrepancy that I cannot explain.
Setup
Rust searcher
local Agave validator / RPC
Yellowstone Geyser
versioned transactions + ALTs
custom Anchor executor
two CPI swaps in the same transaction
leg 2 uses the actual intermediate token balance (use_ib=1) after leg 1
I am not asking about MEV strategy or how to win Jito auctions. The problem already reproduces on my local simulateTransaction of the exact final signed transaction, so I am trying to understand a Solana runtime / CPI / account-state discrepancy.
Repro case
The route is:
Meteora DLMM → PumpSwap
For the first leg:
DLMM custom quoter output: 7,343,589
DLMM reference/SDK output: 7,343,589
diff: 0
The output mint is Token-2022.
I parsed its extensions and it only has:
MetadataPointer
TokenMetadata
There is no TransferFeeConfig, therefore:
expected transfer fee: 0
expected intermediate credit: 7,343,589
The final serialized transaction also contains:
leg2 use_ib = 1
serialized leg2 base_in = 7,343,589
I separately verified the PumpSwap quote against its reference SDK.
Using the pinned Pump state:
reference Pump output = 4,984,165
Using bank-fresh vault state at or before the local simulation slot:
reference Pump output = 4,984,145
So Pump pool state drift is essentially negligible in this case.
However the exact final signed transaction simulated on my local Agave node fails inside PumpSwap with slippage:
LOCAL_SIM context slot = 447250861
runtime Left = 4,908,809
runtime Right = 4,934,324
Jito later sees essentially the same failure:
Left = 4,905,695
Right = 4,934,324
The interesting part is that if I invert the PumpSwap quote using the same pool state, an output of approximately 4,908,809 corresponds to an input of only about:
7,232,560
rather than:
7,343,589
Difference:
111,029 raw tokens
≈ 151 bps
So the current first divergence appears to be:
DLMM reference output
7,343,589
↓
actual intermediate ATA credit / executor IB delta
?
↓
Pump runtime behaves as if input were
~7,232,560
Transfer fees have been ruled out.
The intermediate ATA is JIT-created / empty before the route.
What I am trying to determine
I see three possibilities:
The DLMM CPI actually transfers fewer tokens at runtime than the reference quote, due to the exact execution-bank state.
The DLMM transfer is correct, but my executor calculates the intermediate-balance delta incorrectly.
The intermediate balance is correct, but the amount ultimately passed to PumpSwap differs.
I am now instrumenting the exact signed transaction simulation with innerInstructions=true to extract the actual Token / Token-2022 CPI transfer amount from leg 1.
My questions are:
Is innerInstructions from simulateTransaction reliable for determining the exact Token / Token-2022 transfer amount performed by a CPI before a later instruction fails?
After a CPI changes a token account, are there any known pitfalls when reading that token account again inside the calling Anchor program in the same transaction? For example, does an Anchor Account need an explicit reload() after the CPI to observe the modified amount?
Are there other Solana runtime semantics that could cause an intermediate ATA balance seen by the caller to differ from the amount transferred by the first CPI, assuming there is no transfer fee/hook?
What would you log or inspect next to find the first deterministic divergence between leg1 reference output, actual CPI transfer, intermediate ATA delta, and leg2 input?
I can provide a sanitized decoded transaction, focused logs, account hashes/slots and the small reference-parity harness if useful.
I would especially appreciate answers based on Solana runtime / Anchor CPI account semantics rather than suggestions to widen slippage, because the goal is to explain the exact amount discrepancy.
Quote
Report
Sundram Mahajan · External communityPost link
External answer — Solana Stack Exchange
Author: Sundram Mahajan
Original post: https://solana.stackexchange.com/a/24516
License: CC BY-SA 4.0 — https://creativecommons.org/licenses/by-sa/4.0/
Adaptation: HTML converted to plain text; contact email addresses removed.
Your numbers narrow it down more than you might think.
Left = 4,908,809
vs
Right = 4,934,324
: Right is your minimum (4,984,165 × 0.99, so a 1% tolerance), Left is what PumpSwap computed at runtime, 1.51% under the reference quote, and the gap is the same locally and in the Jito run even though the pool state barely moved. A constant gap that survives different slots is almost never price drift; it is a pricing-model mismatch somewhere in leg 1 or leg 2. Your
innerInstructions
plan will tell you which in one run.
1. Is innerInstructions from simulateTransaction reliable for the CPI transfer amount?
Yes. The simulation runs on one bank snapshot, and inner instructions are recorded for everything that executed before the failing instruction, so leg 1's token CPI is in there even though leg 2 aborted the transaction. Decode the
transferChecked
(Token-2022, tag 12) or
transfer
(tag 3): the u64 amount is at data offset 1, little-endian. Note that
simulateTransaction
returns the
compiled
form (program index, account indexes), so resolve indexes against the message plus your ALTs; the RPC does not parse them for you. Compare that amount with 7,343,589.
2. Reading a token account after a CPI inside Anchor.
An
Account<'info, TokenAccount>
is deserialized once, at instruction entry. After the CPI it is stale until you call
ctx.accounts.intermediate.reload()?
. If your executor takes "pre" from the struct, does the CPI, then takes "post" from the same struct without reloading, the delta is 0 (both reads see the JIT-created empty account). If you instead take "post" from
account_info.data.borrow()
you are fine. A delta of 0 would not produce 7,232,560, so this alone does not explain your case, but check it anyway; it is the most common way the
use_ib
pattern goes wrong.
Account::reload
re-runs the owner and discriminator checks, which is what you want.
3. Where a constant 151 bps most often comes from.
Two candidates, and the inner-instruction amount decides between them:
If leg 1 transferred
less than 7,343,589
: the DLMM reference quote and the on-chain fill disagree. Your quoter and the SDK agree with each other (diff 0), which means both use the same bin-array snapshot; check that the snapshot slot matches the simulation's
context.slot
, and whether the pool's variable fee (the volatility accumulator, which changes with every swap in the bin) was zero in your snapshot but non-zero in the bank. The variable fee alone can be well over 100 bps on a memecoin DLMM pair right after activity.
If leg 1 transferred
exactly 7,343,589
: leg 2's reference quote is wrong, not the input. PumpSwap's fees are not a constant: the on-chain fee comes from the program's global config / fee config and depends on the pool (creator fee, protocol fee, LP fee, and tiering by market cap). A reference quote computed with a hardcoded flat fee will overstate the output by exactly a fee-sized constant, which is what your gap looks like. Quote with the program's own SDK using the fee config fetched on chain at the same slot (
swapSolanaState
then
sellBaseInput
with that state), and re-run the inversion; if the ~111,029 "missing input" turns into a fee, that is your answer.
4. What to log next.
In the executor,
msg!
the pre balance, the post balance after
reload()
, and the exact amount passed to leg 2; in the simulation, the leg-1 transfer amount from
innerInstructions
. Four numbers, one run. Whichever pair disagrees is the first deterministic divergence you are looking for.
For the error itself (PumpSwap's slippage codes and what each one checks):
https://txwhy.vercel.app/errors/pumpswap-amm
. That page is generated from the on-chain IDL, and the same site rebuilds failing swaps from live state if you want a second opinion on the fee side: paste the transaction and compare its recomputed minimum with yours.
Quote
Report
Post Reply
Quoted from Forex.com.bd-Editorial External question — Solana Stack Exchange Author: Aniello Ambrosio Source score (net votes, not local likes): 1 Original post: https://solana.stackexchange.com/questions/24508 License: CC BY-SA 4.0 — https://creativecommons.org/licenses/by-sa/4.0/ Adaptation: HTML converted to plain text; contact email addresses removed. I am debugging an atomic two-leg DEX arbitrage transaction on Solana and I have reached a deterministic discrepancy that I cannot explain. Setup Rust searcher local Agave validator / RPC Yellowstone Geyser versioned transactions + ALTs custom Anchor executor two CPI swaps in the same transaction leg 2 uses the actual intermediate token balance (use_ib=1) after leg 1 I am not asking about MEV strategy or how to win Jito auctions. The problem already reproduces on my local simulateTransaction of the exact final signed transaction, so I am trying to understand a Solana runtime / CPI / account-state discrepancy. Repro case The route is: Meteora DLMM → PumpSwap For the first leg: DLMM custom quoter output: 7,343,589 DLMM reference/SDK output: 7,343,589 diff: 0 The output mint is Token-2022. I parsed its extensions and it only has: MetadataPointer TokenMetadata There is no TransferFeeConfig, therefore: expected transfer fee: 0 expected intermediate credit: 7,343,589 The final serialized transaction also contains: leg2 use_ib = 1 serialized leg2 base_in = 7,343,589 I separately verified the PumpSwap quote against its reference SDK. Using the pinned Pump state: reference Pump output = 4,984,165 Using bank-fresh vault state at or before the local simulation slot: reference Pump output = 4,984,145 So Pump pool state drift is essentially negligible in this case. However the exact final signed transaction simulated on my local Agave node fails inside PumpSwap with slippage: LOCAL_SIM context slot = 447250861 runtime Left = 4,908,809 runtime Right = 4,934,324 Jito later sees essentially the same failure: Left = 4,905,695 Right = 4,934,324 The interesting part is that if I invert the PumpSwap quote using the same pool state, an output of approximately 4,908,809 corresponds to an input of only about: 7,232,560 rather than: 7,343,589 Difference: 111,029 raw tokens ≈ 151 bps So the current first divergence appears to be: DLMM reference output 7,343,589 ↓ actual intermediate ATA credit / executor IB delta ? ↓ Pump runtime behaves as if input were ~7,232,560 Transfer fees have been ruled out. The intermediate ATA is JIT-created / empty before the route. What I am trying to determine I see three possibilities: The DLMM CPI actually transfers fewer tokens at runtime than the reference quote, due to the exact execution-bank state. The DLMM transfer is correct, but my executor calculates the intermediate-balance delta incorrectly. The intermediate balance is correct, but the amount ultimately passed to PumpSwap differs. I am now instrumenting the exact signed transaction simulation with innerInstructions=true to extract the actual Token / Token-2022 CPI transfer amount from leg 1. My questions are: Is innerInstructions from simulateTransaction reliable for determining the exact Token / Token-2022 transfer amount performed by a CPI before a later instruction fails? After a CPI changes a token account, are there any known pitfalls when reading that token account again inside the calling Anchor program in the same transaction? For example, does an Anchor Account need an explicit reload() after the CPI to observe the modified amount? Are there other Solana runtime semantics that could cause an intermediate ATA balance seen by the caller to differ from the amount transferred by the first CPI, assuming there is no transfer fee/hook? What would you log or inspect next to find the first deterministic divergence between leg1 reference output, actual CPI transfer, intermediate ATA delta, and leg2 input? I can provide a sanitized decoded transaction, focused logs, account hashes/slots and the small reference-parity harness if useful. I would especially appreciate answers based on Solana runtime / Anchor CPI account semantics rather than suggestions to widen slippage, because the goal is to explain the exact amount discrepancy.
Checking account access…