Implement synthetic token validation and enhance swap logic in GMX plugin

- Added early validation to prevent swapping synthetic tokens, ensuring users are informed about the limitations of synthetic tokens.
- Enhanced the swap logic to handle synthetic tokens by falling back to a direct swap order transaction when synthetic tokens are involved or when the SDK swap fails.
- Improved the calculation of minimum output amounts based on swap path statistics or fallback to price-based calculations for better accuracy.
This commit is contained in:
2026-01-02 23:45:32 +07:00
parent 16421a1c9c
commit 78c2788ba7
2 changed files with 151 additions and 65 deletions

View File

@@ -35,6 +35,9 @@ import {approveContractImpl, getTokenAllowance} from './privy.js';
import {estimateExecuteSwapOrderGasLimit} from '../../generated/gmxsdk/utils/fees/executionFee.js'; import {estimateExecuteSwapOrderGasLimit} from '../../generated/gmxsdk/utils/fees/executionFee.js';
import {getExecutionFee} from '../../generated/gmxsdk/utils/fees/executionFee.js'; import {getExecutionFee} from '../../generated/gmxsdk/utils/fees/executionFee.js';
import {estimateOrderOraclePriceCount} from '../../generated/gmxsdk/utils/fees/estimateOraclePriceCount.js'; import {estimateOrderOraclePriceCount} from '../../generated/gmxsdk/utils/fees/estimateOraclePriceCount.js';
import {createFindSwapPath} from '../../generated/gmxsdk/utils/swap/swapPath.js';
import {SwapPricingType} from '../../generated/gmxsdk/types/orders.js';
import {convertToUsd} from '../../generated/gmxsdk/utils/tokens.js';
import { import {
Position, Position,
PositionStatus, PositionStatus,
@@ -2949,14 +2952,36 @@ export const swapGmxTokensImpl = async (
toTicker, toTicker,
fromTokenAddress: fromTokenData?.address, fromTokenAddress: fromTokenData?.address,
fromTokenIsNative: fromTokenData?.isNative, fromTokenIsNative: fromTokenData?.isNative,
fromTokenIsSynthetic: fromTokenData?.isSynthetic,
toTokenAddress: toTokenData?.address, toTokenAddress: toTokenData?.address,
toTokenIsNative: toTokenData?.isNative toTokenIsNative: toTokenData?.isNative,
toTokenIsSynthetic: toTokenData?.isSynthetic
}); });
if (!fromTokenData || !toTokenData) { if (!fromTokenData || !toTokenData) {
throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`); throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`);
} }
// EARLY VALIDATION: Check for synthetic tokens before any transaction preparation
// Synthetic tokens cannot be swapped to/from - they can only be obtained through positions
if (toTokenData.isSynthetic) {
const marketConfig = getMarketByIndexToken(toTokenData.address);
const marketInfo = marketConfig ? `Market: ${marketConfig.marketTokenAddress}` : 'No market found';
throw new Error(
`Cannot swap to synthetic token ${toTokenData.symbol}. ` +
`Synthetic tokens are index tokens and can only be obtained by opening a long position. ` +
`Please use the open-position endpoint instead. ${marketInfo}`
);
}
if (fromTokenData.isSynthetic) {
throw new Error(
`Cannot swap from synthetic token ${fromTokenData.symbol}. ` +
`Synthetic tokens cannot be used as input for swaps. ` +
`Please close your position first to convert the synthetic token to collateral.`
);
}
// Check for zero addresses (but allow native tokens like ETH) // Check for zero addresses (but allow native tokens like ETH)
if (fromTokenData.address === "0x0000000000000000000000000000000000000000" && !fromTokenData.isNative) { if (fromTokenData.address === "0x0000000000000000000000000000000000000000" && !fromTokenData.isNative) {
throw new Error(`From token ${fromTicker} has zero address - token not found or invalid`); throw new Error(`From token ${fromTicker} has zero address - token not found or invalid`);
@@ -3122,44 +3147,95 @@ export const swapGmxTokensImpl = async (
toTicker toTicker
}); });
// Try using the SDK's built-in swap method first // Check if either token is synthetic - SDK swap doesn't support synthetic tokens
try { const hasSyntheticToken = fromTokenData.isSynthetic || toTokenData.isSynthetic;
const swapParams = {
fromAmount: verifiedFromTokenAmount,
fromTokenAddress: fromTokenData.address,
toTokenAddress: toTokenData.address,
allowedSlippageBps: allowedSlippageBps,
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
skipSimulation: true,
} as SwapParams;
swapParams.marketsInfoData = marketsInfoData; if (hasSyntheticToken) {
swapParams.tokensData = tokensData; console.log('🔄 Synthetic token detected, using direct createSwapOrderTxn (SDK swap does not support synthetic tokens):', {
fromToken: fromTokenData.symbol,
console.log('🔄 Attempting SDK swap with slippage:', { fromIsSynthetic: fromTokenData.isSynthetic,
allowedSlippageBps: allowedSlippageBps, toToken: toTokenData.symbol,
slippagePercentage: slippagePercentage toIsSynthetic: toTokenData.isSynthetic
}); });
await sdk.orders.swap(swapParams); } else {
console.log('✅ SDK swap successful!'); // Try using the SDK's built-in swap method first (only for non-synthetic tokens)
} catch (sdkError) { try {
console.log('❌ SDK swap failed, trying alternative approach:', sdkError); const swapParams = {
fromAmount: verifiedFromTokenAmount,
fromTokenAddress: fromTokenData.address,
toTokenAddress: toTokenData.address,
allowedSlippageBps: allowedSlippageBps,
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
skipSimulation: true,
} as SwapParams;
// Fall back to createSwapOrderTxn directly with simpler parameters swapParams.marketsInfoData = marketsInfoData;
console.log('🔄 Attempting direct createSwapOrderTxn...'); swapParams.tokensData = tokensData;
// Calculate execution fee dynamically console.log('🔄 Attempting SDK swap with slippage:', {
const swapPath = [fromTokenData.address, toTokenData.address]; allowedSlippageBps: allowedSlippageBps,
// For a direct swap (token A -> token B), there's 1 swap through 1 market slippagePercentage: slippagePercentage
// The swapPath length represents the number of markets/swaps in the path });
// For a simple direct swap, we estimate 1 swap await sdk.orders.swap(swapParams);
const swapsCount = 1; console.log('✅ SDK swap successful!');
const gasLimits = await sdk.utils.getGasLimits(); return "swap_order_created";
const gasPrice = await sdk.utils.getGasPrice(); } catch (sdkError) {
console.log('❌ SDK swap failed, trying alternative approach:', sdkError);
if (!gasLimits || gasPrice === undefined) {
throw new Error("Failed to get gas limits or gas price for execution fee calculation");
} }
}
// Fall back to createSwapOrderTxn directly (for synthetic tokens or if SDK swap fails)
console.log('🔄 Attempting direct createSwapOrderTxn...');
// Get gas limits and price first (needed for swap path finding)
const gasLimits = await sdk.utils.getGasLimits();
const gasPrice = await sdk.utils.getGasPrice();
if (!gasLimits || gasPrice === undefined) {
throw new Error("Failed to get gas limits or gas price for execution fee calculation");
}
// Find the proper swap path using createFindSwapPath (works for both synthetic and non-synthetic tokens)
const findSwapPath = createFindSwapPath({
chainId: sdk.chainId,
fromTokenAddress: fromTokenData.address,
toTokenAddress: toTokenData.address,
marketsInfoData,
gasEstimationParams: {
gasLimits,
gasPrice,
tokensData,
},
swapPricingType: SwapPricingType.Swap,
});
// Calculate USD amount for swap path finding
const fromTokenPrice = fromTokenData.prices?.minPrice
? Number(fromTokenData.prices.minPrice) / 1e30
: 1;
const usdIn = convertToUsd(verifiedFromTokenAmount, fromTokenData.decimals, BigInt(Math.floor(fromTokenPrice * 1e30))) || 0n;
// Find the swap path
const swapPathStats = findSwapPath(usdIn);
let swapPath: string[];
let swapsCount: number;
if (swapPathStats && swapPathStats.swapPath) {
swapPath = swapPathStats.swapPath;
swapsCount = swapPath.length - 1; // Number of swaps = path length - 1
console.log('✅ Found swap path:', {
path: swapPath,
swapsCount,
usdOut: swapPathStats.usdOut?.toString(),
amountOut: swapPathStats.amountOut?.toString()
});
} else {
// No swap path found - this should not happen for valid token pairs
// (Synthetic tokens are already validated early, so we won't reach here for them)
console.warn('⚠️ No swap path found, using direct path (may not work for all token pairs)');
swapPath = [fromTokenData.address, toTokenData.address];
swapsCount = 1;
}
const estimatedGasLimit = estimateExecuteSwapOrderGasLimit(gasLimits, { const estimatedGasLimit = estimateExecuteSwapOrderGasLimit(gasLimits, {
swapsCount: swapsCount, swapsCount: swapsCount,
@@ -3183,32 +3259,43 @@ export const swapGmxTokensImpl = async (
console.log(`💰 Calculated execution fee: ${(Number(executionFee.feeTokenAmount) / 1e18).toFixed(6)} ETH`); console.log(`💰 Calculated execution fee: ${(Number(executionFee.feeTokenAmount) / 1e18).toFixed(6)} ETH`);
// Calculate minOutputAmount based on current market price and slippage // Calculate minOutputAmount based on swap path stats or fallback to price-based calculation
// Get the current price ratio from token data let minOutputAmount: bigint;
const fromTokenPrice = fromTokenData.prices?.minPrice
? Number(fromTokenData.prices.minPrice) / 1e30
: 1;
const toTokenPrice = toTokenData.prices?.minPrice
? Number(toTokenData.prices.minPrice) / 1e30
: 1;
// Calculate expected output amount (simplified - assumes 1:1 price ratio if prices unavailable) if (swapPathStats && swapPathStats.amountOut) {
const fromAmountInUsd = (Number(verifiedFromTokenAmount) / Math.pow(10, fromTokenData.decimals)) * fromTokenPrice; // Use the amount from swap path stats (more accurate)
const expectedOutputInUsd = fromAmountInUsd * (toTokenPrice / fromTokenPrice); const slippageMultiplier = BigInt(10000 - allowedSlippageBps); // e.g., 9950 for 0.5% slippage
const expectedOutputInTokens = expectedOutputInUsd / toTokenPrice; minOutputAmount = (swapPathStats.amountOut * slippageMultiplier) / 10000n;
console.log('💱 Using swap path stats for minOutputAmount:', {
amountOut: swapPathStats.amountOut.toString(),
minOutputAmount: minOutputAmount.toString(),
slippageBps: allowedSlippageBps
});
} else {
// Fallback to price-based calculation
const fromTokenPrice = fromTokenData.prices?.minPrice
? Number(fromTokenData.prices.minPrice) / 1e30
: 1;
const toTokenPrice = toTokenData.prices?.minPrice
? Number(toTokenData.prices.minPrice) / 1e30
: 1;
// Apply slippage tolerance to minimum output const fromAmountInUsd = (Number(verifiedFromTokenAmount) / Math.pow(10, fromTokenData.decimals)) * fromTokenPrice;
const minOutputAmount = BigInt(Math.floor( const expectedOutputInUsd = fromAmountInUsd * (toTokenPrice / fromTokenPrice);
expectedOutputInTokens * Math.pow(10, toTokenData.decimals) * (1 - slippagePercentage / 100) const expectedOutputInTokens = expectedOutputInUsd / toTokenPrice;
));
console.log('💱 Swap output calculation:', { // Apply slippage tolerance to minimum output
fromAmountUsd: fromAmountInUsd.toFixed(2), minOutputAmount = BigInt(Math.floor(
expectedOutputUsd: expectedOutputInUsd.toFixed(2), expectedOutputInTokens * Math.pow(10, toTokenData.decimals) * (1 - slippagePercentage / 100)
expectedOutputTokens: expectedOutputInTokens.toFixed(6), ));
minOutputAmount: minOutputAmount.toString(), console.log('💱 Using price-based calculation for minOutputAmount (fallback):', {
slippagePercentage: slippagePercentage fromAmountUsd: fromAmountInUsd.toFixed(2),
}); expectedOutputUsd: expectedOutputInUsd.toFixed(2),
expectedOutputTokens: expectedOutputInTokens.toFixed(6),
minOutputAmount: minOutputAmount.toString(),
slippagePercentage: slippagePercentage
});
}
await createSwapOrderTxn(sdk, { await createSwapOrderTxn(sdk, {
fromTokenAddress: fromTokenData.address, fromTokenAddress: fromTokenData.address,
@@ -3224,7 +3311,6 @@ export const swapGmxTokensImpl = async (
skipSimulation: true, skipSimulation: true,
}); });
console.log('✅ Direct createSwapOrderTxn successful!'); console.log('✅ Direct createSwapOrderTxn successful!');
}
return "swap_order_created"; return "swap_order_created";
}, sdk, 0 }, sdk, 0

View File

@@ -13,9 +13,9 @@ describe('swap tokens implementation', () => {
const result = await swapGmxTokensImpl( const result = await swapGmxTokensImpl(
sdk, sdk,
Ticker.SOL,
Ticker.USDC, Ticker.USDC,
0.0495, Ticker.RENDER,
5, // 5 USDC
'market', 'market',
undefined, undefined,
0.5 0.5