Skip to main content
Glama
clumsynonono

Aave Liquidation MCP Server

by clumsynonono
aave-client.ts13.3 kB
/** * Aave V3 Client for fetching protocol data */ import { ethers } from 'ethers'; import { AAVE_V3_POOL_ADDRESS, AAVE_V3_POOL_DATA_PROVIDER, AAVE_V3_ORACLE, AAVE_POOL_ABI, AAVE_DATA_PROVIDER_ABI, AAVE_ORACLE_ABI, ERC20_ABI, LIQUIDATION_THRESHOLD, WARNING_THRESHOLD, } from './constants.js'; import { UserAccountData, UserReserveData, ReserveData, LiquidationOpportunity, BatchAnalysisResult, } from './types.js'; export class AaveClient { private provider: ethers.Provider; private poolContract: ethers.Contract; private dataProviderContract: ethers.Contract; private oracleContract: ethers.Contract; // Cache for reserves data (reduces RPC calls) private reserveCache: ReserveData[] | null = null; private reserveCacheTime: number = 0; private readonly CACHE_TTL = 60000; // 1 minute cache constructor(rpcUrl: string) { this.provider = new ethers.JsonRpcProvider(rpcUrl); this.poolContract = new ethers.Contract( AAVE_V3_POOL_ADDRESS, AAVE_POOL_ABI, this.provider ); this.dataProviderContract = new ethers.Contract( AAVE_V3_POOL_DATA_PROVIDER, AAVE_DATA_PROVIDER_ABI, this.provider ); this.oracleContract = new ethers.Contract( AAVE_V3_ORACLE, AAVE_ORACLE_ABI, this.provider ); } /** * Get user account data including health factor */ async getUserAccountData(userAddress: string): Promise<UserAccountData> { const data = await this.poolContract.getUserAccountData(userAddress); const healthFactorFormatted = ethers.formatUnits(data.healthFactor, 18); const healthFactorNumber = parseFloat(healthFactorFormatted); return { address: userAddress, totalCollateralBase: data.totalCollateralBase, totalDebtBase: data.totalDebtBase, availableBorrowsBase: data.availableBorrowsBase, currentLiquidationThreshold: data.currentLiquidationThreshold, ltv: data.ltv, healthFactor: data.healthFactor, healthFactorFormatted, isLiquidatable: healthFactorNumber < LIQUIDATION_THRESHOLD && healthFactorNumber > 0, isAtRisk: healthFactorNumber < WARNING_THRESHOLD && healthFactorNumber > 0, }; } /** * Get user reserves (collateral and debt positions) * Optimized with parallel queries for better performance */ async getUserReserves(userAddress: string): Promise<{ collateral: UserReserveData[]; debt: UserReserveData[]; }> { const reserves = await this.getAllReserves(); const collateral: UserReserveData[] = []; const debt: UserReserveData[] = []; // Parallel query all user reserves const userReservePromises = reserves.map(reserve => this.dataProviderContract.getUserReserveData(reserve.tokenAddress, userAddress) ); const userReservesData = await Promise.all(userReservePromises); // Process results for (let i = 0; i < reserves.length; i++) { const reserve = reserves[i]; const userReserve = userReservesData[i]; const totalDebt = userReserve.currentStableDebt + userReserve.currentVariableDebt; if (userReserve.currentATokenBalance > 0n || totalDebt > 0n) { // Use cached decimals from reserves const decimals = reserve.decimals; const data: UserReserveData = { asset: reserve.tokenAddress, symbol: reserve.symbol, currentATokenBalance: userReserve.currentATokenBalance, currentStableDebt: userReserve.currentStableDebt, currentVariableDebt: userReserve.currentVariableDebt, usageAsCollateralEnabled: userReserve.usageAsCollateralEnabled, decimals, balanceFormatted: ethers.formatUnits(userReserve.currentATokenBalance, decimals), debtFormatted: ethers.formatUnits(totalDebt, decimals), liquidationBonus: reserve.liquidationBonus, }; if (userReserve.currentATokenBalance > 0n) { collateral.push(data); } if (totalDebt > 0n) { debt.push(data); } } } return { collateral, debt }; } /** * Get all available reserves in Aave V3 * Optimized with caching and parallel queries */ async getAllReserves(): Promise<ReserveData[]> { // Return cached data if still valid const now = Date.now(); if (this.reserveCache && (now - this.reserveCacheTime) < this.CACHE_TTL) { return this.reserveCache; } const reserveTokens = await this.dataProviderContract.getAllReservesTokens(); // Parallel query all configurations and decimals const configPromises = reserveTokens.map((reserve: { tokenAddress: string }) => this.dataProviderContract.getReserveConfigurationData(reserve.tokenAddress) ); const decimalsPromises = reserveTokens.map((reserve: { tokenAddress: string }) => { const tokenContract = new ethers.Contract(reserve.tokenAddress, ERC20_ABI, this.provider); return tokenContract.decimals(); }); const [configs, decimalsList] = await Promise.all([ Promise.all(configPromises), Promise.all(decimalsPromises), ]); const reserves: ReserveData[] = reserveTokens.map( (reserve: { symbol: string; tokenAddress: string }, index: number) => ({ symbol: reserve.symbol, tokenAddress: reserve.tokenAddress, decimals: Number(decimalsList[index]), ltv: configs[index].ltv, liquidationThreshold: configs[index].liquidationThreshold, liquidationBonus: configs[index].liquidationBonus, usageAsCollateralEnabled: configs[index].usageAsCollateralEnabled, borrowingEnabled: configs[index].borrowingEnabled, isActive: configs[index].isActive, }) ); // Update cache this.reserveCache = reserves; this.reserveCacheTime = now; return reserves; } /** * Get asset price from Aave oracle */ async getAssetPrice(assetAddress: string): Promise<string> { const price = await this.oracleContract.getAssetPrice(assetAddress); // Oracle returns price with 8 decimals return ethers.formatUnits(price, 8); } /** * Get prices for multiple assets */ async getAssetsPrices(assetAddresses: string[]): Promise<Map<string, string>> { const prices = await this.oracleContract.getAssetsPrices(assetAddresses); const priceMap = new Map<string, string>(); for (let i = 0; i < assetAddresses.length; i++) { priceMap.set(assetAddresses[i], ethers.formatUnits(prices[i], 8)); } return priceMap; } /** * Analyze a user position for liquidation opportunity */ async analyzeLiquidationOpportunity( userAddress: string ): Promise<LiquidationOpportunity | null> { const accountData = await this.getUserAccountData(userAddress); // Only return if liquidatable or at risk if (!accountData.isLiquidatable && !accountData.isAtRisk) { return null; } const { collateral, debt } = await this.getUserReserves(userAddress); // Calculate risk level const hf = parseFloat(accountData.healthFactorFormatted); let riskLevel: 'HIGH' | 'MEDIUM' | 'LOW'; if (hf < LIQUIDATION_THRESHOLD) { riskLevel = 'HIGH'; } else if (hf < 1.02) { riskLevel = 'MEDIUM'; } else { // 1.02 <= hf < WARNING_THRESHOLD (1.05) riskLevel = 'LOW'; } // Format values in USD (base is already in USD with 8 decimals from oracle) const totalCollateralUSD = ethers.formatUnits(accountData.totalCollateralBase, 8); const totalDebtUSD = ethers.formatUnits(accountData.totalDebtBase, 8); const availableBorrowsUSD = ethers.formatUnits(accountData.availableBorrowsBase, 8); // Calculate potential profit with actual liquidation bonus let potentialProfit = '0'; if (accountData.isLiquidatable) { const debtValue = parseFloat(totalDebtUSD); // Close factor: can liquidate up to 50% of debt const maxDebtByCloseFactor = debtValue * 0.5; // Fetch prices for collateral assets to avoid overstating profit const priceMap = collateral.length ? await this.getAssetsPrices(collateral.map((c) => c.asset)) : new Map<string, string>(); let bestEstimatedProfit = 0; for (const c of collateral) { const priceStr = priceMap.get(c.asset); const price = priceStr ? parseFloat(priceStr) : 0; if (!price) { continue; } // Convert collateral balance to USD using token decimals and oracle price (8 decimals) const collateralAmount = parseFloat(ethers.formatUnits(c.currentATokenBalance, c.decimals)); const collateralValueUSD = collateralAmount * price; // liquidationBonus is in bps, e.g. 10500 => 5% bonus const bonusPercentage = Math.max(0, Number(c.liquidationBonus) - 10000) / 10000; if (bonusPercentage <= 0) { continue; } // Debt that can be covered by this collateral considering bonus const maxDebtByCollateral = collateralValueUSD / (1 + bonusPercentage); // Actual liquidatable debt for this collateral const liquidatableDebt = Math.min(maxDebtByCloseFactor, maxDebtByCollateral, debtValue); const estimatedProfit = liquidatableDebt * bonusPercentage; if (estimatedProfit > bestEstimatedProfit) { bestEstimatedProfit = estimatedProfit; } } potentialProfit = bestEstimatedProfit.toFixed(2); } return { userAddress, healthFactor: accountData.healthFactorFormatted, totalCollateralUSD, totalDebtUSD, availableBorrowsUSD, liquidationThreshold: parseFloat( ethers.formatUnits(accountData.currentLiquidationThreshold, 4) ), collateralAssets: collateral, debtAssets: debt, potentialProfit, riskLevel, gasWarning: 'Profit calculation does not include Gas costs. Actual profit will be lower.', }; } /** * Get current block number */ async getBlockNumber(): Promise<number> { return await this.provider.getBlockNumber(); } /** * Batch analyze multiple addresses for liquidation opportunities */ async batchAnalyzeLiquidation( addresses: string[] ): Promise<BatchAnalysisResult[]> { const results: BatchAnalysisResult[] = new Array(addresses.length); const concurrency = Math.min(5, addresses.length); let currentIndex = 0; const worker = async () => { while (true) { const idx = currentIndex++; if (idx >= addresses.length) { break; } const address = addresses[idx]; try { const opportunity = await this.analyzeLiquidationOpportunity(address); results[idx] = { address, opportunity }; } catch (error) { results[idx] = { address, opportunity: null, error: error instanceof Error ? error.message : String(error), }; } } }; await Promise.all(Array.from({ length: concurrency }, () => worker())); return results; } /** * Validate Ethereum address format */ isValidAddress(address: string): boolean { return ethers.isAddress(address); } /** * Get reserve statistics including total supply and borrow */ async getReserveStats(assetAddress: string): Promise<{ symbol: string; totalSupply: string; totalBorrow: string; utilizationRate: string; supplyAPY: string; borrowAPY: string; } | null> { try { const reserves = await this.getAllReserves(); const reserve = reserves.find((r) => r.tokenAddress.toLowerCase() === assetAddress.toLowerCase()); if (!reserve) { return null; } const { aTokenAddress, variableDebtTokenAddress, stableDebtTokenAddress } = await this.dataProviderContract.getReserveTokensAddresses(assetAddress); const aTokenContract = new ethers.Contract(aTokenAddress, ERC20_ABI, this.provider); const totalSupply = await aTokenContract.totalSupply(); const reserveData = await this.dataProviderContract.getReserveData(assetAddress); // Calculate total borrow (stable + variable) const totalStableDebt = reserveData.totalStableDebt; const totalVariableDebt = reserveData.totalVariableDebt; const totalBorrow = totalStableDebt + totalVariableDebt; // Calculate utilization rate const totalSupplyVal = parseFloat(ethers.formatUnits(totalSupply, reserve.decimals)); const totalBorrowVal = parseFloat(ethers.formatUnits(totalBorrow, reserve.decimals)); let utilizationRate = '0'; if (totalSupplyVal > 0) { utilizationRate = (totalBorrowVal / totalSupplyVal).toFixed(4); } // APYs are in ray (1e27); keep as decimal fractions (e.g. 0.05 for 5%) const supplyAPY = parseFloat(ethers.formatUnits(reserveData.liquidityRate, 27)).toFixed(4); const borrowAPY = parseFloat(ethers.formatUnits(reserveData.variableBorrowRate, 27)).toFixed(4); return { symbol: reserve.symbol, totalSupply: ethers.formatUnits(totalSupply, reserve.decimals), totalBorrow: ethers.formatUnits(totalBorrow, reserve.decimals), utilizationRate, supplyAPY, borrowAPY, }; } catch (error) { return null; } } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/clumsynonono/aave-liquidation-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server