diff --git a/src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md b/src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md new file mode 100644 index 00000000..dd579ab8 --- /dev/null +++ b/src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md @@ -0,0 +1,114 @@ +# Synthetic Token Fix for GMX Swaps + +## Problem +When attempting to swap tokens to/from BTC on GMX, users were encountering the following error: +``` +Cannot swap to synthetic token BTC. Synthetic tokens are index tokens and can only be obtained by opening a long position. Please use the open-position endpoint instead. +``` + +## Root Cause +In the GMX token configuration (`src/generated/gmxsdk/configs/tokens.ts`), there are **two BTC tokens**: + +1. **Synthetic BTC** (Index Token) + - Symbol: `BTC` + - Address: `0x47904963fc8b2340414262125aF798B9655E58Cd` + - `isSynthetic: true` + - Cannot be swapped directly + - Can only be obtained by opening a long position + +2. **Wrapped Bitcoin (WBTC)** + - Symbol: `BTC` + - Asset Symbol: `WBTC` + - Base Symbol: `BTC` + - Address: `0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f` + - `isSynthetic: false` (or not set) + - **CAN be swapped** + +Both tokens have the same symbol "BTC", which was causing the wrong token to be selected for swaps. + +## Solution +Updated the `getTokenDataFromTicker()` function to **explicitly filter out synthetic tokens** when fetching tokens for swaps by using the `isSynthetic: false` parameter in `getTokenBySymbol()`. + +### Code Changes + +#### File: `src/plugins/custom/gmx.ts` + +**Function: `getTokenDataFromTicker()`** + +Added the `isSynthetic: false` filter to all `getTokenBySymbol()` calls: + +```typescript +// First try v2 tokens with explicit isSynthetic: false filter +token = getTokenBySymbol(arbitrum.id, ticker, { version: "v2", isSynthetic: false }); + +// Fallback: try without version constraint but still filter synthetic tokens +token = getTokenBySymbol(arbitrum.id, ticker, { isSynthetic: false }); + +// Last resort: try to find by baseSymbol +token = getTokenBySymbol(arbitrum.id, ticker, { + isSynthetic: false, + symbolType: "baseSymbol" +}); +``` + +### How It Works + +1. When a user requests to swap BTC (or any token), the system now: + - Calls `getTokenBySymbol()` with `isSynthetic: false` + - This filter ensures only non-synthetic tokens are returned + - For BTC, it will return the Wrapped Bitcoin (WBTC) token instead of the synthetic BTC token + +2. The filter is applied at the token lookup level, ensuring: + - **Swaps**: Always use non-synthetic tokens (can be swapped) + - **Positions**: Can still use synthetic index tokens (via `getMarketInfoFromTicker()`) + +## Testing + +### Test Case +Added a test in `test/plugins/swap-tokens.test.ts` to verify: +- USDC can be swapped to BTC +- The system automatically selects the non-synthetic WBTC token +- The swap completes without throwing the synthetic token error + +```typescript +it('should swap USDC to BTC successfully (BTC resolves to non-synthetic WBTC token)', async () => { + const result = await swapGmxTokensImpl( + sdk, + 'USDC', + 'BTC', // isSynthetic: false filter selects the non-synthetic WBTC token + 5, + 'market', + undefined, + 0.5 + ); + assert.strictEqual(result, 'swap_order_created'); +}); +``` + +## Impact + +### Before Fix +- Swaps to/from BTC would fail with synthetic token error +- Users had to manually specify "WBTC" instead of "BTC" + +### After Fix +- Users can use "BTC" in swap requests +- System automatically uses the correct non-synthetic WBTC token +- No more synthetic token errors for swaps +- Opening positions still works correctly with synthetic index tokens + +## Key Takeaways + +1. **Always filter synthetic tokens for swaps**: Use `isSynthetic: false` when calling `getTokenBySymbol()` for swap operations +2. **Token symbol ambiguity**: Multiple tokens can have the same symbol but different properties (synthetic vs non-synthetic) +3. **Proper token selection**: The `isSynthetic` filter is crucial for selecting the correct token for different operations +4. **Following GMX patterns**: The GMX SDK itself uses `isSynthetic: false` in markets.ts (lines 1205-1210) when fetching long/short tokens + +## Related Files +- `src/plugins/custom/gmx.ts` - Main swap implementation +- `src/generated/gmxsdk/configs/tokens.ts` - Token configuration +- `test/plugins/swap-tokens.test.ts` - Test cases + +## Date +January 6, 2026 + diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 6339f8a5..d597cc72 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -1233,6 +1233,10 @@ function getMarketInfoFromTicker(ticker: string, marketsInfoData: MarketsInfoDat ticker = "WETH"; } + // Note: For BTC, we don't need manual conversion anymore since getTokenBySymbol + // will automatically select the non-synthetic token when we pass isSynthetic: false + // However, for market info lookups, we may need the synthetic index token + // So we don't filter synthetic tokens here (markets can have synthetic index tokens) const token = getTokenBySymbol(arbitrum.id, ticker); const marketInfo = getMarketByIndexToken(token.address); @@ -1251,22 +1255,43 @@ function getMarketInfoFromTicker(ticker: string, marketsInfoData: MarketsInfoDat export function getTokenDataFromTicker(ticker: string, tokensData: TokensData): TokenData { console.log(`šŸ” Looking up token for ticker: ${ticker}`); - // Try to find the token without synthetic filter to support both synthetic and non-synthetic tokens - // First try v2 tokens (preferred) + + // IMPORTANT: For swaps, we must ONLY use non-synthetic tokens (isSynthetic: false) + // Synthetic tokens (like BTC at 0x47904963fc8b2340414262125aF798B9655E58Cd) cannot be swapped + // We need to use wrapped tokens (like WBTC at 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f) instead + // The isSynthetic filter ensures we get the correct token let token; try { - token = getTokenBySymbol(arbitrum.id, ticker, { version: "v2" }); + // First try v2 tokens with explicit isSynthetic: false filter + token = getTokenBySymbol(arbitrum.id, ticker, { version: "v2", isSynthetic: false }); } catch (error) { - // If not found in v2, try without version constraint - token = getTokenBySymbol(arbitrum.id, ticker); + // If not found in v2, try without version constraint but still filter synthetic tokens + try { + token = getTokenBySymbol(arbitrum.id, ticker, { isSynthetic: false }); + } catch (innerError) { + // Last resort: try to find by assetSymbol (e.g., WBTC for BTC) + // This handles cases where the user passes "BTC" but we need to find the token with assetSymbol "WBTC" + try { + token = getTokenBySymbol(arbitrum.id, ticker, { + isSynthetic: false, + symbolType: "baseSymbol" + }); + } catch (finalError) { + throw new Error(`Token not found for ticker: ${ticker}. Please ensure you're using a valid non-synthetic token symbol.`); + } + } } + console.log(`šŸ“‹ Token found:`, { symbol: token.symbol, + assetSymbol: token.assetSymbol, + baseSymbol: token.baseSymbol, address: token.address, decimals: token.decimals, isNative: token.isNative, isSynthetic: token.isSynthetic }); + const tokenData = getByKey(tokensData, token.address); console.log(`šŸ“Š Token data:`, { address: tokenData?.address, @@ -1274,6 +1299,7 @@ export function getTokenDataFromTicker(ticker: string, tokensData: TokensData): isNative: tokenData?.isNative, symbol: tokenData?.symbol }); + return tokenData; } @@ -1939,8 +1965,6 @@ export const getSpotPositionHistoryImpl = async ( positions.push(position); } - console.log(`šŸ“Š Positions:`, positions); - if (ticker) { positions = positions.filter(p => p.ticker === (ticker as any) || diff --git a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts index 6bc7406a..40d9ec0b 100644 --- a/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts +++ b/src/Managing.Web3Proxy/test/plugins/swap-tokens.test.ts @@ -32,4 +32,36 @@ describe('swap tokens implementation', () => { assert.fail(error.message) } }) + + it('should swap USDC to BTC successfully (BTC resolves to non-synthetic WBTC token)', async () => { + try { + const testAccount = '0x932167388dD9aad41149b3cA23eBD489E2E2DD78' + + const sdk = await getClientForAddress(testAccount) + + console.log('\nšŸ”„ Starting USDC → BTC swap...\n') + console.log('Note: BTC ticker resolves to the non-synthetic WBTC token (address: 0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f)') + console.log('The synthetic BTC token (address: 0x47904963fc8b2340414262125aF798B9655E58Cd) is filtered out\n') + + // Swap $5 worth of USDC to BTC + // The isSynthetic: false filter ensures we get the wrapped BTC (WBTC) token + // At ~$100k BTC price, this should give us ~0.00005 WBTC + const result = await swapGmxTokensImpl( + sdk, + 'USDC', + 'BTC', // isSynthetic: false filter selects the non-synthetic WBTC token + 5, // $5 worth of USDC + 'market', + undefined, + 0.5 + ) + + console.log('\nāœ… USDC → BTC swap successful! Used non-synthetic WBTC token\n') + assert.strictEqual(typeof result, 'string') + assert.strictEqual(result, 'swap_order_created') + } catch (error) { + console.log('\nāŒ USDC → BTC swap failed:', error) + assert.fail(error.message) + } + }) })