Swap Direction — The a_to_b and b_to_a Trap
Every swap has a direction. This sounds so obvious that it barely seems worth mentioning. You're trading token A for token B, or token B for token A. Two directions. Same pool. Simple enough. I build support for one direction, it works perfectly, and I move on to the next feature with the confidence of someone who just parallel-parked on the first try.
Then the bot starts running in production, and the vast majority of the opportunities it identifies end in failed transactions. Not occasionally. Not under edge conditions. Half. Consistently. Like a highway with two lanes, except one lane has a concrete barrier across it that nobody put up a warning sign for.
The error messages are unhelpful. Something about an account constraint violation. Something about an incorrect owner. Nothing that points clearly at the root cause. The transactions that succeed all share one characteristic: they happen to be swapping in the direction I built and tested first. The transactions that fail all share another: they're going the other way. It takes longer than I'd like to admit before I connect these dots.
The One-Way Street That Looks Like a Two-Way
Here's the mental model that gets me into trouble. A liquidity pool holds two tokens — let's call them SOL and USDC. The pool has a constant product formula, reserves on both sides, and a fee rate. When I think about "supporting this pool," I think about understanding its math, parsing its on-chain state, and constructing swap instructions for it. One pool, one set of math, one implementation. Done.
This is like looking at a street and seeing pavement. Yes, it's pavement in both directions. But driving east and driving west on that same street involve different lane markings, different traffic signals, different turn restrictions, and critically, different on-ramps. The road surface is the same. Everything else is direction-dependent.
A DEX swap instruction doesn't just say "swap these two tokens in this pool." It says "swap this specific amount of this specific input token for the other token, using these specific accounts in this specific order." The instruction knows which direction you're going. The pool contract knows which direction you're going. And the accounts you pass to the instruction must match that direction.
This is where the trap springs.
The Source Account Problem
Every swap instruction needs to know where to pull the input tokens from and where to deposit the output tokens. On Solana, these are Associated Token Accounts — ATAs. Each wallet has a separate ATA for each token it holds. My wallet has a WSOL ATA, a USDC ATA, a BONK ATA, and so on. When I swap WSOL for USDC, the instruction pulls WSOL from my WSOL ATA and deposits USDC into my USDC ATA. When I swap USDC for WSOL, the instruction pulls USDC from my USDC ATA and deposits WSOL into my WSOL ATA.
The source changes. The destination changes. The direction of the swap inverts which account is the source and which is the destination.
This feels manageable when stated abstractly. Two directions, two source accounts, swap them based on direction. Three lines of conditional logic. A first-semester CS student handles this in their sleep.
But here's where the real-world implementation diverges from the abstract description. On Solana, a swap instruction doesn't take named parameters like source_account and destination_account with clear labels. It takes a flat list of accounts in a specific order defined by the program. Account at position zero might be the pool. Account at position one might be the authority. Account at position five might be the source token account. Account at position six might be the destination token account.
There are no labels at runtime. Just positions. The program expects the source token account at a specific index in the accounts array. If you put the wrong account at that index, the transaction fails. And the direction of the swap determines which of your ATAs is the source.
When I first build the swap instruction for a given DEX, I get it working for one direction. A-to-B. SOL-to-USDC. Whatever the first test case happens to be. I put my WSOL ATA at the source position because that's the source for this direction. The swap succeeds. The math checks out. I move on.
Then the bot encounters the reverse direction — B-to-A — and constructs the instruction with the exact same account layout. My WSOL ATA is still at the source position. But now WSOL isn't the input. It's the output. That position needs the USDC ATA. The instruction tries to pull WSOL from my WSOL ATA as if it's USDC, the program's constraints detect the mismatch, and the transaction fails with a cryptic error about account ownership or token mint mismatch.
It's the difference between walking into a bank and saying "transfer from checking to savings" versus "transfer from savings to checking." Same two accounts, same bank, same customer. But if the teller's form has a fixed field for "source account" and you always write your checking account number there, half your transfers go to the wrong place.
The Subtlety That Compounds
If swaps only ever went in one direction, this wouldn't matter. Build it once, test it once, deploy it. But cyclic arbitrage is inherently multi-directional. A cycle like SOL → USDC → BONK → SOL involves three swaps, and each one has its own direction relative to the pool it's using.
The SOL/USDC pool might see the first swap as A-to-B. The USDC/BONK pool might see the second swap as A-to-B as well. But the BONK/SOL pool might see the third swap as B-to-A, depending on how the pool defines its internal token ordering. Which token is "A" and which is "B" is determined by the pool's program, not by me. Different DEXes have different conventions. Some order alphabetically by mint address. Some order by which token was specified first when the pool was created. Some have explicit token_a_mint and token_b_mint fields in the pool's on-chain state.
I don't get to choose the direction. The cycle dictates it. When my bot identifies a profitable cycle SOL → USDC → BONK → SOL, it doesn't get to decide that every hop should be A-to-B. Each hop's direction is fixed by the relationship between the swap's input token and the pool's internal ordering. If SOL is token B in the SOL/BONK pool, then the final hop — BONK to SOL — is an A-to-B swap. But if SOL is token A in that pool, the final hop is B-to-A.
The bot doesn't control direction. It has to handle whatever direction each hop requires. And if the instruction builder assumes a fixed direction — if the source ATA index is hardcoded for A-to-B — then every B-to-A hop fails.
In a three-hop cycle, the probability that all three hops happen to be the same direction is low. The probability that at least one hop requires the reverse direction is high. Which means a direction-handling bug doesn't cause occasional failures. It causes pervasive failures. Every cycle that includes even one B-to-A hop — which is most cycles — fails entirely. The whole transaction reverts. Not just the broken hop. All of it.
The Debugging Fog
What makes this bug particularly insidious is the error message. When a Solana transaction fails because the wrong account is at a specific index, the error typically references a constraint violation in the program. "Account at index 5 does not match expected mint." Or worse, a generic error code that maps to "ConstraintTokenMint" or "InvalidAccountData" in the program's error table.
These error messages are technically accurate. The account at index five doesn't match the expected mint. But they don't tell you why. They don't say "you passed the SOL ATA but this is a B-to-A swap so we expected the USDC ATA." They just say "wrong account." And there are dozens of reasons an account might be wrong — wrong pool state, stale data, incorrect ATA derivation, wrong owner, closed account, insufficient balance.
When I first encounter these failures, I chase the wrong hypotheses. Maybe the pool state is stale. I add more aggressive state refresh logic. The failures continue. Maybe the ATA derivation is wrong. I double-check the derivation math. It's correct. Maybe the account is closed or has insufficient balance. I verify the account exists and has funds. It does.
I'm checking under every lamppost except the one where I dropped my keys. The source ATA is correct for one direction and wrong for the other, and I'm not looking at direction because I "already handled" the swap instruction. It works, doesn't it? I tested it. It worked fine.
It worked fine in one direction.
This is the homeowner who tests their new security system by locking the front door and trying to open it. Works perfectly. Great security system. Except they never tested the back door, and the back door uses a different lock, and that lock was never installed. The house is secure from the front. Wide open from the back. And they don't discover this until someone walks in through the back.
The Fix That Reveals the Pattern
Once I identify the root cause, the fix is straightforward. When building the swap instruction, check the swap direction. If A-to-B, the source ATA is the token-A account. If B-to-A, the source ATA is the token-B account. Swap them in the accounts array based on direction.
The code change is small. A conditional. Maybe a swap of two variables. Trivially simple once you know it's needed.
But the fix reveals a deeper pattern that applies across every DEX integration. It's not just the source ATA that changes with direction. Depending on the DEX program, other accounts might change too. Some programs have direction-specific vault accounts. Some have different oracle accounts for different directions. Some have different fee accounts. Each DEX has its own opinion about what's direction-dependent and what's shared.
This means that for every new DEX I integrate, I need to think about direction from the start. Not as an afterthought. Not as a "I'll handle the reverse direction later." The direction-dependent accounts must be identified during the initial integration, and both directions must be tested before I consider the integration complete.
It's the same principle behind the DMV making you demonstrate both left and right turns during a driving test. They don't let you pass because you turned left perfectly and assume you can figure out right turns on your own. Both directions, demonstrated and verified, before you get the license.
The Per-DEX Variation
The direction problem manifests differently on each DEX, and this variation is what makes it genuinely difficult rather than just tedious.
On some DEXes, the pool maintains separate vault accounts for each token. Token A reserves live in Vault A. Token B reserves live in Vault B. The swap instruction references both vaults, but the "input vault" and "output vault" swap based on direction. If A-to-B, the input vault is Vault A. If B-to-A, the input vault is Vault B. The instruction's accounts array might need these vaults in a specific order — input vault before output vault — which means the order of vault accounts in the array flips with direction.
On other DEXes, the pool uses a single unified vault, and the direction is encoded as a flag in the instruction data rather than in the accounts array. Same accounts for both directions, different instruction payload. The source ATA still changes, but the vault accounts don't.
On concentrated liquidity DEXes, the direction determines which tick arrays are relevant. Swapping A-to-B might traverse ticks in one direction — moving the current price downward through the tick space. Swapping B-to-A traverses ticks in the opposite direction. The tick array accounts passed to the instruction might need to be different based on direction, or at minimum, the instruction needs to know which direction to traverse.
Each DEX is its own puzzle. The common thread is that direction matters and it's easy to overlook, but the specific way it matters varies per program. There's no universal rule like "just swap the source and destination ATAs." Sometimes that's all you need. Sometimes you need to reorder vault accounts. Sometimes you need to change instruction flags. Sometimes you need different auxiliary accounts entirely.
This is what makes the "it works in one direction" bug so pernicious. You can fix it for one DEX and still have it lurking in another DEX integration where the direction-dependent accounts are different. The pattern is universal. The implementation is per-DEX.
What Half the Opportunities Looks Like
The impact of this bug on a cyclic arbitrage bot is severe and asymmetric. It's not like losing a coin flip where you win some and lose some. It's like running a delivery business where your truck can only turn right. You can still make some deliveries — the ones that happen to involve only right turns. But every route that requires even one left turn is impossible. And since most routes involve turns in both directions, you lose most of your routes.
In my case, the bot scans pools, identifies profitable cycles, and constructs multi-hop swap transactions. Each cycle typically involves two to four hops across different pools. Each hop has a direction that's determined by the cycle structure, not by my choice. The directions are essentially random from the bot's perspective — determined by the internal token ordering of each pool, which varies across DEXes and even across pools on the same DEX.
With the direction bug present, any cycle containing at least one B-to-A hop fails. In a three-hop cycle, assuming random direction for each hop, the probability that all three happen to match your tested direction is low. The vast majority of cycles fail. The longer the cycle, the more devastating the bug.
The actual ratio depends on the specific pool ordering and cycle structure, but the point stands: direction bugs don't reduce your success rate linearly. They compound across hops. Each hop is an independent chance for the direction mismatch to kill the entire transaction. More hops, more chances for failure. The longer the cycle, the more devastating the bug.
And here's the cruel part: you don't know you're missing these opportunities unless you audit the failures. The bot reports successful executions and they look reasonable. A handful of captures per day. Seems like it's working. But if you look at the failed transactions — really look, not just count them — you realize that the failure rate is dramatically higher than the success rate, and the common thread among failures is direction-related account mismatches. The bot isn't finding a handful of opportunities a day. It's finding many times more, but silently failing on most of them.
This is the retail store that processes returns but not exchanges. Customers who want to return an item for cash? Smooth, easy, works every time. Customers who want to exchange for a different size? System error. Come back later. The store doesn't realize it's losing half its customer service interactions because the return counter is always busy and appears to be functioning normally.
Testing Both Directions
The lesson crystallizes into a simple discipline: test both directions for every DEX integration, from the start, before anything else.
Not "I'll test A-to-B first and then get to B-to-A later." Not "A-to-B works, so B-to-A is probably fine." Both directions. Verified. Before the integration is considered complete.
This sounds obvious after you've been burned by the bug. Before being burned, it feels like unnecessary thoroughness. A-to-B works. The math is the same in both directions. The pool doesn't care which direction you trade. Why wouldn't B-to-A work?
Because the accounts change. Because the instruction layout might change. Because a vault ordering might change. Because a flag might need to be set. Because the math is indeed the same in both directions, but the instruction that invokes that math is not.
I build a testing checklist for new DEX integrations. Step one: identify the pool's internal token ordering. Step two: construct an A-to-B swap instruction and verify it succeeds. Step three: construct a B-to-A swap instruction with the same pool and verify it succeeds. Step four: construct a multi-hop cycle that requires both directions across different pools and verify the full cycle succeeds.
If any step fails, the integration isn't done. If step two succeeds but step three fails, there's a direction-dependent account that isn't being handled. Find it, fix it, retest both directions.
The discipline costs maybe thirty minutes of additional testing per DEX integration. The cost of not doing it is weeks of running a bot that silently fails on the majority of its identified opportunities, burning compute and transaction fees on doomed transactions while the operator looks at the success count and thinks everything is fine.
The Broader Pattern
The swap direction trap is a specific instance of a broader pattern in systems programming: asymmetric testing creates asymmetric failures. Whenever a system handles two symmetric cases — request and response, encode and decode, serialize and deserialize, encrypt and decrypt, A-to-B and B-to-A — there's a natural tendency to test one side thoroughly and assume the other side works because the logic is "the same."
It's never the same. The logic might be symmetric, but the implementation details rarely are. Different code paths, different account references, different index calculations, different edge cases. The symmetry exists at the conceptual level. At the implementation level, each direction is its own thing that needs its own verification.
Highway on-ramps and off-ramps serve the same conceptual purpose — they connect local roads to the highway. But they're different physical structures with different merge patterns, different speed requirements, and different sight lines. You wouldn't assume an on-ramp works just because you tested the off-ramp at the same interchange. They share a location, not a design.
In cyclic arbitrage, this asymmetry is lethal because cycles are multi-hop. A bug that affects one direction doesn't just fail one swap — it poisons every cycle that includes a swap in that direction. And since the bot can't choose which direction each hop takes, the bug is not something you can work around by avoiding certain cycles. It's embedded in the structure of the opportunity space.
The fix is simple. The testing discipline is simple. The hard part is adopting the discipline before the bug teaches it to you the expensive way. Every direction matters. Every direction needs its own test. A swap that works one way and fails the other isn't a swap that half-works. It's a swap that doesn't work, with a demo mode that happens to look convincing.
I keep a sticky note on my monitor now. Not literally — my monitor doesn't have a sticky note. But metaphorically, burned into my process. "Did you test both directions?" Five words. Thirty minutes of testing. Saves weeks of silent failures and the uncomfortable realization that my bot has been running at a fraction of its potential because I assumed that what works going east also works going west.
It doesn't. Test both ways. Always.
Disclaimer
This article is for informational and educational purposes only and does not constitute financial, investment, legal, or professional advice. Content is produced independently and supported by advertising revenue. While we strive for accuracy, this article may contain unintentional errors or outdated information. Readers should independently verify all facts and data before making decisions. Company names and trademarks are referenced for analysis purposes under fair use principles. Always consult qualified professionals before making financial or legal decisions.