Blog
← Back to Blog

The $2,600 Bug That Wasn't a Bug

11 min read

Three weeks into testing. We were running real transfers through the engine side-by-side with manual checks before actually executing. ETH to SOL, $100K USDC. Engine picks Route B. Route A is $300 cheaper on paper.

Opened a ticket: "scorer is picking suboptimal routes on large transfers." Priority: high. Assigned to ourselves because there's nobody else. Spent two days stepping through the scoring logic line by line. Re-read the evaluation weights. Double-checked the fee calculations. Triple-checked the slippage model.

Everything was correct. The engine was doing exactly what we told it to do. We just hadn't fully internalized what that meant yet.

The Test Case

$100K USDC, Ethereum to Solana. After pruning, two serious candidates remained:

text
1Route A — deBridge direct
2 quoted fees: $82
3 quoted slippage: 0.08% ($80)
4 quoted speed: ~25s (DLN intent fill)
5 expected output: $99,838
6 pool depth: $1.2M available
7
8Route B — Wormhole direct
9 quoted fees: $145
10 quoted slippage: 0.03% ($30)
11 quoted speed: ~4min (guardian consensus)
12 expected output: $99,825
13 pool depth: $18M+ (deep liquidity)

Route A looks better by $13 in expected output. Faster too. Every aggregator would put it at the top. We would have picked it manually.

Our engine picked Route B. We filed a bug.

What We Missed

Then we actually ran the worst-case analysis by hand. This is what the engine was doing internally — we just hadn't looked at the intermediate numbers:

typescript
1const STRESS = {
2 slippage: 2.0, // 2x quoted slippage
3 gasSurge: 1.5, // 50% gas increase
4 bridgeDelay: 3.0, // 3x quoted time
5 mevExtraction: 0.003, // 0.3% extracted
6 priceMovement: 0.005, // 0.5% adverse move during transit
7};
8
9// Route A — deBridge, $1.2M pool
10// $100K into a $1.2M pool is 8.3% of total liquidity.
11// at 2x stress, slippage model gives:
12const slippageA = poolSlippage(100_000, 1_200_000) * STRESS.slippage;
13// = 0.08% * 2.0 = 0.16%... but that's the linear estimate.
14// constant product formula at this pool ratio:
15// actual stressed slippage = ~3.8% ($3,800)
16// pool is too shallow for this size under stress.
17
18routeA.worstCase = 100_000 - 82 - 3_800 - 300 - 500 = 95_318;
19
20// Route B — Wormhole, $18M+ pool
21// $100K into $18M is 0.55% of liquidity. barely moves the pool.
22// even at 2x stress, slippage stays under 0.1%
23const slippageB = poolSlippage(100_000, 18_000_000) * STRESS.slippage;
24// = 0.03% * 2.0 = 0.06% ($60)
25
26routeB.worstCase = 100_000 - 145 - 60 - 300 - 500 = 98_995;

There it is. Route A worst case: $95,318. Route B worst case: $98,995. The difference: $3,677.

Route A's pool was $1.2M. Putting $100K through it means you're 8.3% of the pool. Under normal conditions, fine — the constant product curve handles it. Under stress (2x multiplier), the slippage curve goes nonlinear and eats you alive. That's how constant product AMMs work — they're fine until you're a significant fraction of the pool, then the curve gets brutal.

Route B's pool was $18M+. $100K is a rounding error. Even under stress, slippage barely registers. Higher base fees, but the pool depth protects you.

We Almost Changed It

This is the part that's hard to admit. We actually wrote a PR to switch the default scoring to expected value. Branch name:fix/expected-value-scoring. The reasoning was "users expect cheapest-first, this will confuse people, we should match what aggregators do."

The PR was reviewed. The code was clean. It passed tests (because we'd written the tests against expected-value logic, which tells you how deep the assumption went). It was ready to merge.

Then someone said "let's just backtest it first." So we did.

text
1backtest: 6 weeks of historical transfers
2 transfers analyzed: 312
3 transfers > $10K: 89
4
5 worst-case scoring vs expected-value scoring on transfers > $10K:
6 worst-case wins: 65 (73.0%)
7 expected-value wins: 18 (20.2%)
8 tied (same route): 6 (6.7%)
9
10 average improvement when worst-case wins:
11 all transfers > $10K: ~$820
12 transfers > $50K: ~$1,400
13 transfers > $100K: ~$2,180
14
15 average loss when expected-value wins:
16 all transfers > $10K: ~$95
17 (expected-value "wins" were almost always marginal)

73% of the time, worst-case scoring found a better route. And when it won, it won big — $820 average, $2,180 on large transfers. When expected-value won, the margin was tiny.

Closed the PR. Deleted the branch. The engine stays as-is.

Why Expected Value Breaks Here

Expected value needs probability distributions. "Slippage will be 0.1% with 70% probability, 0.5% with 25%, 2% with 5%." In traditional finance you sometimes have decades of data to build those distributions. In cross-chain you have maybe months of data for bridges that are constantly changing their infrastructure.

And the distributions you'd need aren't normal. Bridge delays are bimodal — either on time or catastrophically late, almost nothing in between. Slippage correlates with exactly the conditions that make you want low slippage (big transfer + thin pool = bad time). MEV is adversarial — literally someone trying to extract value from your transaction.

Worst-case doesn't assume any distribution. It asks a simpler question: "if conditions get 2x worse across the board, which route still holds up?" That's answerable without probability estimates.

Tuning the Multipliers

The stress multipliers aren't arbitrary. We calibrated them against six months of bridge data — looking at how often real conditions exceeded quoted conditions, and by how much.

text
1slippage multiplier calibration:
2 % of transfers where actual slippage > 1.5x quoted: 18.4%
3 % of transfers where actual slippage > 2.0x quoted: 6.2%
4 % of transfers where actual slippage > 3.0x quoted: 1.1%
5
6 chose 2.0x — captures 93.8% of real-world slippage events.
7 aggressive traders can lower to 1.5x (covers 81.6%).
8 institutions can raise to 3.0x (covers 98.9%).

2.0x is the default because it's the sweet spot — conservative enough to catch almost all real slippage events, not so conservative that it picks absurdly expensive routes just for safety.

Strategy Profiles

Despite being confident in worst-case scoring, we added alternative strategies. Not everyone has the same priorities:

typescript
1const route = await router.findRoute({
2 from: { chain: 'ethereum', token: 'USDC', amount: '100000' },
3 to: { chain: 'solana', token: 'USDC' },
4 strategy: 'minimax', // default — guaranteed minimum
5});
6
7// alternatives:
8// 'cheapest' — lowest expected fees, period
9// 'fastest' — minimize transfer time
10// 'safest' — maximize bridge reliability score

Same search engine, same data, same bridge adapters. Just a different objective function. We think minimax is right for most transfers above $10K. But it's not our money. If you genuinely want cheapest-first, it's there.