Enhance token handling and logging in GMX plugin
- Updated token retrieval logic to ensure non-synthetic tokens are prioritized for swaps, improving accuracy in token selection. - Added detailed logging for token data, including assetSymbol and baseSymbol, to enhance visibility during token lookups. - Introduced a new test case to validate the successful swap of USDC to BTC, confirming the resolution to the non-synthetic WBTC token. - Improved error handling for token lookups, providing clearer feedback when a valid token symbol is not found.
This commit is contained in:
114
src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md
Normal file
114
src/Managing.Web3Proxy/SYNTHETIC-TOKEN-FIX.md
Normal file
@@ -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
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user