Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,12 @@ export class AgentClient {
};

/**
* Stellar Classic DEX routing.
* Stellar Classic DEX routing with enhanced slippage protection.
*
* These methods use Horizon pathfinding and path payment operations, which can
* route through the SDEX and built-in liquidity pools.
*
* πŸ›‘οΈ SECURITY: Enhanced with slippage protection and MEV defense
*/
public dex = {
quoteSwap: async (params: QuoteSwapParams): Promise<RouteQuote[]> => {
Expand All @@ -270,16 +272,35 @@ export class AgentClient {
);
},

/**
* Execute best route swap with advanced slippage protection
*
* πŸ›‘οΈ SECURITY FEATURES:
* - Automatic slippage optimization based on market conditions
* - Price impact analysis and warnings
* - MEV (sandwich attack) protection
* - Route validation and security scoring
*
* @param params Swap parameters with optional protection settings
* @returns Swap result with protection information
*/
swapBestRoute: async (
params: SwapBestRouteParams
): Promise<SwapBestRouteResult> => {
// πŸ›‘οΈ SECURITY: Enable slippage protection by default
const enhancedParams = {
...params,
enableSlippageProtection: params.enableSlippageProtection ?? true,
maxPriceImpactBps: params.maxPriceImpactBps ?? 300, // 3% default max price impact
};

return await executeBestRouteSwap(
{
network: this.network,
horizonUrl: this.rpcUrl,
publicKey: this.publicKey,
},
params
enhancedParams
);
},
};
Expand Down
94 changes: 85 additions & 9 deletions lib/dex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@stellar/stellar-sdk";
import { getSigningKeypair, signTransaction } from "./stellar";
import { buildPathPaymentTransaction } from "../utils/buildTransaction";
import { SlippageProtectionManager, validateSlippage } from "./slippageProtection";

export type StellarAssetInput =
| { type: "native" }
Expand All @@ -27,6 +28,8 @@ export interface QuoteSwapParams {

export interface SwapBestRouteParams extends QuoteSwapParams {
slippageBps?: number;
enableSlippageProtection?: boolean; // πŸ›‘οΈ NEW: Enable advanced slippage protection
maxPriceImpactBps?: number; // πŸ›‘οΈ NEW: Maximum acceptable price impact
}

export interface HorizonPathRecord {
Expand Down Expand Up @@ -56,6 +59,10 @@ export interface SwapBestRouteResult {
sendAmount: string;
destAmount: string;
path: StellarAssetInput[];
// πŸ›‘οΈ NEW: Enhanced result information
actualSlippageBps?: number;
priceImpactBps?: number;
protectionWarnings?: string[];
}

export interface DexClientConfig {
Expand Down Expand Up @@ -157,21 +164,34 @@ export function calculateSwapBounds(
mode: RouteMode,
slippageBps: number = DEFAULT_SLIPPAGE_BPS
): { sendMax?: string; destMin?: string } {
// πŸ›‘οΈ SECURITY: Enhanced slippage validation
validateSlippage(slippageBps, 1000); // Max 10% slippage allowed

if (!Number.isInteger(slippageBps) || slippageBps < 0) {
throw new Error("slippageBps must be a non-negative integer");
throw new Error("πŸ›‘οΈ slippageBps must be a non-negative integer");
}

const slippage = new Big(slippageBps).div(10000);

if (mode === "strict-send") {
return {
destMin: formatStellarAmount(new Big(quote.destAmount).mul(new Big(1).minus(slippage))),
};
const destMin = formatStellarAmount(new Big(quote.destAmount).mul(new Big(1).minus(slippage)));

// πŸ›‘οΈ SECURITY: Validate minimum output is reasonable
if (new Big(destMin).lte(0)) {
throw new Error("πŸ›‘οΈ Calculated minimum output is zero or negative - slippage too high");
}

return { destMin };
}

return {
sendMax: formatStellarAmount(new Big(quote.sendAmount).mul(new Big(1).plus(slippage))),
};
const sendMax = formatStellarAmount(new Big(quote.sendAmount).mul(new Big(1).plus(slippage)));

// πŸ›‘οΈ SECURITY: Validate maximum input is reasonable
if (new Big(sendMax).gte(new Big(quote.sendAmount).mul(2))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Dead/unreachable guard: the 2x maximum-input check can never trigger because slippage is capped at 10%.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At lib/dex.ts, line 190:

<comment>Dead/unreachable guard: the 2x maximum-input check can never trigger because slippage is capped at 10%.</comment>

<file context>
@@ -157,21 +164,34 @@ export function calculateSwapBounds(
+  const sendMax = formatStellarAmount(new Big(quote.sendAmount).mul(new Big(1).plus(slippage)));
+  
+  // πŸ›‘οΈ SECURITY: Validate maximum input is reasonable
+  if (new Big(sendMax).gte(new Big(quote.sendAmount).mul(2))) {
+    throw new Error("πŸ›‘οΈ Calculated maximum input is unreasonably high - check slippage settings");
+  }
</file context>

throw new Error("πŸ›‘οΈ Calculated maximum input is unreasonably high - check slippage settings");
}

return { sendMax };
}

export async function quoteSwap(
Expand Down Expand Up @@ -214,6 +234,10 @@ export async function swapBestRoute(
validateQuoteParams(params);
validateQuoteLimit(params.limit);

// πŸ›‘οΈ SECURITY: Enhanced slippage validation with protection
const requestedSlippageBps = params.slippageBps ?? DEFAULT_SLIPPAGE_BPS;
validateSlippage(requestedSlippageBps, 1000); // Max 10% slippage

getSigningKeypair(client.publicKey);

const destination = params.destination ?? client.publicKey;
Expand All @@ -223,7 +247,55 @@ export async function swapBestRoute(
const bestQuote = quotes[0];

if (!bestQuote) {
throw new Error("No route available for the requested swap");
throw new Error("πŸ›‘οΈ No route available for the requested swap");
}

// πŸ›‘οΈ SECURITY: Apply slippage protection if enabled
let finalSlippageBps = requestedSlippageBps;
let protectionWarnings: string[] = [];
let priceImpactBps = 0;

if (params.enableSlippageProtection !== false) { // Default to enabled
const slippageProtection = new SlippageProtectionManager({
maxSlippageBps: 1000, // 10% absolute maximum
priceImpactWarningThreshold: params.maxPriceImpactBps ?? 200, // 2% default warning threshold
});

const tradeAmount = params.mode === "strict-send" ? params.sendAmount! : bestQuote.sendAmount;

const protection = slippageProtection.analyzeSlippageProtection(
bestQuote,
requestedSlippageBps,
tradeAmount,
params.sendAsset,
params.destAsset
);

if (!protection.shouldProceed) {
throw new Error(
`πŸ›‘οΈ Trade blocked by slippage protection:\n` +
`${protection.warnings.join('\n')}\n` +
`Consider reducing trade size or adjusting parameters.`
);
}

finalSlippageBps = protection.adjustedSlippageBps;
protectionWarnings = protection.warnings;
priceImpactBps = protection.priceImpact.priceImpactBps;

// Log protection warnings
if (protectionWarnings.length > 0) {
console.warn("πŸ›‘οΈ Slippage Protection Warnings:");
protectionWarnings.forEach(warning => console.warn(` ${warning}`));
}

// Additional price impact check
if (params.maxPriceImpactBps && priceImpactBps > params.maxPriceImpactBps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: maxPriceImpactBps is not validated and the max-price-impact check uses a truthy guard, so 0/NaN bypass protection and negative values behave incorrectly.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At lib/dex.ts, line 293:

<comment>`maxPriceImpactBps` is not validated and the max-price-impact check uses a truthy guard, so `0`/`NaN` bypass protection and negative values behave incorrectly.</comment>

<file context>
@@ -223,7 +247,55 @@ export async function swapBestRoute(
+    }
+
+    // Additional price impact check
+    if (params.maxPriceImpactBps && priceImpactBps > params.maxPriceImpactBps) {
+      throw new Error(
+        `πŸ›‘οΈ Price impact ${priceImpactBps / 100}% exceeds maximum allowed ${params.maxPriceImpactBps / 100}%.\n` +
</file context>

throw new Error(
`πŸ›‘οΈ Price impact ${priceImpactBps / 100}% exceeds maximum allowed ${params.maxPriceImpactBps / 100}%.\n` +
`${protection.priceImpact.recommendation}`
);
}
}

const createServer =
Expand All @@ -234,7 +306,7 @@ export async function swapBestRoute(
const { sendMax, destMin } = calculateSwapBounds(
bestQuote,
params.mode,
params.slippageBps ?? DEFAULT_SLIPPAGE_BPS
finalSlippageBps // Use protected slippage
);

const transaction = buildPathPaymentTransaction(sourceAccount, {
Expand Down Expand Up @@ -268,6 +340,10 @@ export async function swapBestRoute(
sendAmount: params.mode === "strict-send" ? params.sendAmount! : bestQuote.sendAmount,
destAmount: params.mode === "strict-receive" ? params.destAmount! : bestQuote.destAmount,
path: bestQuote.path,
// πŸ›‘οΈ NEW: Include protection information
actualSlippageBps: finalSlippageBps,
priceImpactBps: priceImpactBps || undefined,
protectionWarnings: protectionWarnings.length > 0 ? protectionWarnings : undefined,
};
}

Expand Down
Loading