Skip to content

Swap bundles 400 on stringsInstructions 128-char limit, making 39 of 186 GalaSwap pools unswappable #828

Description

@snichols

Summary

Since ~2026-08-31, GalaSwap POST /bundle rejects Swap submissions for any pool whose token composite keys are long enough that a derived stringsInstructions entry exceeds 128 characters:

(400) each value in stringsInstructions must be shorter than or equal to 128 characters

This is not a caller error. The offending strings are constructed deterministically from the pool's own token class keys — by gswap-sdk itself — so the limit makes a subset of GalaSwap's own live pools structurally unswappable. 39 of 186 pools (21%) currently returned by explore/pools cannot be swapped, including pools holding real TVL.

The strings are derived, not caller-supplied

gswap-sdk/src/classes/swaps.ts:115-128 builds all five entries from the pool identity:

const poolString        = `$pool$${token0StringKey}$${token1StringKey}$${fee}`;
const tokenBalance0     = `$tokenBalance$${token0StringKey}$${walletAddress}`;
const tokenBalance1     = `$tokenBalance$${token1StringKey}$${walletAddress}`;
const tokenBalance0Pool = `$tokenBalance$${token0StringKey}$${poolString}`;
const tokenBalance1Pool = `$tokenBalance$${token1StringKey}$${poolString}`;

tokenBalance{0,1}Pool embed a token key and poolString, which already contains both token keys — so the longest entry grows as 2 × len(token0) + len(token1) + 23. A caller has no way to shorten it.

Concrete, using only public data (GALA/MOON, 1% fee):

$tokenBalance$Token$Unit$MOON$eth:321485B12C6b7aEd3888D240Df3e3E8Ee79e7393$$pool$GALA$Unit$none$none$Token$Unit$MOON$eth:321485B12C6b7aEd3888D240Df3e3E8Ee79e7393$10000

167 characters. Rejected.

The trigger is launchpad/bridged tokens whose composite keys carry a creator suffix. GALA$Unit$none$none is 19 chars; Token$Unit$MOON$eth:321485B12C6b7aEd3888D240Df3e3E8Ee79e7393 is 60. Two ordinary keys fit comfortably; one long key does not.

Scope

Computed over all 186 pools from GET /explore/pools, using each pool's own token0CompositeKey / token1CompositeKey / fee and a standard 45-char eth|… wallet address:

  • 39 of 186 pools (21.0%) produce at least one instruction over 128 chars
  • Worst case: FIM/SMOOV at 209 chars
  • Affected pairs include GALA/MOON (167), GALA/BENE (141/140), GALA/DKP (164), GALA/FGC (139), GALA/TSP (139), GALA/STU (165), GALA/FIM (165), GALA/SMOOV (169), GALA/GRILLZ (170), GALA/ALIEN (168), GALA/PUMPX (168), GUSDC/MOON, GUSDT/MOON, GUSDC/BENE, GUSDT/BENE, BENE/MOON (195)

These pools accept liquidity and are listed as tradeable, but every swap against them 400s.

Timeline

These pools swapped successfully as recently as 2026-08-11 (confirmed against our own execution history, 19/19 successful swaps through GALA/MOON and GALA/BENE pools). First stringsInstructions rejection observed 2026-09-01 02:52:48 UTC. 401 rejections since, 100% of attempts. No change on our side in that window.

Suspected cause — hypothesis, not a diagnosis

I can't see where the validator is declared (stringsInstructions doesn't appear in this repo — it's the bundler request envelope), so this may belong on the bundler or GalaChain/dex instead; happy to move it.

But the timing lines up with the #811 strict-whitelist rollout:

when (UTC) what
2026-08-26 10:33 v3.2.0 — #811 "Reject unknown DTO properties during validation"
2026-08-26 11:55 GalaChain/dex #89 "Mark optional liquidity DTO fields callers already send"
2026-08-27 17:05 GalaChain/dex v1.1.4 pins @gala-chain/api 3.2.0
2026-08-27 09:59 v3.2.1 — #819 mark optional oracle DTO filters for whitelist
2026-08-31 14:30 v3.2.2 — #821 add back legacy balance field
2026-08-31 18:39 v3.2.3 — #823 remove validation on balance responses
2026-09-01 02:52 first stringsInstructions 128-char rejection
2026-09-02 13:39 #825 merged — "the third face of the #811 strict-whitelist rollout"

#825 describes the mechanism precisely: "Whitelist validation only recognizes properties that carry at least one validator decorator, so an undecorated property is 'unknown' even on its own class."

If stringsInstructions was previously undecorated, #811 would have made it fail whitelist validation, and the fix would be to add a decorator. My guess is that the decorator added was @MaxLength(128, { each: true }) — the error text is that decorator's verbatim class-validator output, and the each value in prefix is exactly how class-validator renders { each: true }. If so, 128 was a default reached for rather than a deliberate protocol limit, and this would be the fourth face of the same rollout.

Expected behaviour

Either:

  1. The cap is raised or removed for stringsInstructions — the entries are protocol-derived state keys whose length is a function of token composite keys the platform itself issues, so no fixed cap that is shorter than the longest constructible key is safe; or
  2. If a cap is genuinely intended, GalaSwap should not list pools whose swaps can never validate — and gswap-sdk should surface the constraint at quote/route time rather than as a 400 at submit.

Environment

  • Bundler: https://bundle-backend-prod1.defi.gala.com/bundle
  • Backend: https://dex-backend-prod1.defi.gala.com
  • Client: a Go implementation constructing the identical five stringsInstructions entries as gswap-sdk/src/classes/swaps.ts
  • @gala-chain/dex on main is v1.1.4, pinning @gala-chain/api 3.2.0

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions