UNPKG

flashloan-profit-calculator

Version:

A library for analyzing flashloan transactions and calculating profits

322 lines (321 loc) 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ETHERSCAN_API_KEY = exports.COINGECKO_API_KEY = void 0; exports.getImplementationAddress = getImplementationAddress; exports.isWethLikeToken = isWethLikeToken; exports.findWethLikeTokens = findWethLikeTokens; exports.getEventNameFromTopic = getEventNameFromTopic; exports.decodeEventLog = decodeEventLog; exports.generateEventSignature = generateEventSignature; exports.getContractABI = getContractABI; exports.getABIAndDecodeLog = getABIAndDecodeLog; const alchemy_sdk_1 = require("alchemy-sdk"); const dotenv_1 = __importDefault(require("dotenv")); const constants_1 = require("./constants"); const ethers_1 = require("ethers"); dotenv_1.default.config(); exports.COINGECKO_API_KEY = process.env.COINGECKO_API_KEY; exports.ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY; // Initialize Alchemy const settings = { apiKey: process.env.ALCHEMY_API_KEY, network: alchemy_sdk_1.Network.ETH_MAINNET, }; const alchemy = new alchemy_sdk_1.Alchemy(settings); // EIP-1967 implementation slot const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; /** * Get the implementation address of a proxy contract */ async function getImplementationAddress(proxyAddress) { try { const implementation = await alchemy.core.getStorageAt(proxyAddress, IMPLEMENTATION_SLOT); if (implementation && implementation !== "0x0000000000000000000000000000000000000000000000000000000000000000") { return `0x${implementation.slice(-40)}`; // Convert to address format } return null; } catch (error) { console.error(`Error getting implementation address for ${proxyAddress}: ${error}`); return null; } } /** * Check if a token is WETH-like by examining its contract code */ async function isWethLikeToken(tokenAddress) { try { // Skip if already known if (constants_1.WETH_LIKE_TOKENS.has(tokenAddress)) { return true; } // Get contract code const code = await alchemy.core.getCode(tokenAddress); if (!code || code === "0x") { return false; } // First check the contract directly const hasWethFunctions = constants_1.WETH_FUNCTION_SIGNATURES.some((sig) => code.includes(sig)); if (hasWethFunctions) { // Add to known WETH-like tokens constants_1.WETH_LIKE_TOKENS.add(tokenAddress); return true; } // If not found in direct code, check if this is a proxy contract const implementationAddress = await getImplementationAddress(tokenAddress); if (implementationAddress) { // Check the implementation contract const implementationCode = await alchemy.core.getCode(implementationAddress); if (!implementationCode || implementationCode === "0x") { return false; } console.log("implementationCode ", implementationAddress); // Check for WETH-like function signatures in implementation const hasWethFunctionsInImpl = constants_1.WETH_FUNCTION_SIGNATURES.some((sig) => implementationCode.includes(sig)); if (hasWethFunctionsInImpl) { // Add to known WETH-like tokens constants_1.WETH_LIKE_TOKENS.add(tokenAddress); console.log("hasWethFunctionsInImpl ", hasWethFunctionsInImpl); return true; } } return false; } catch (error) { console.error(`Error checking WETH-like token ${tokenAddress}: ${error}`); return false; } } /** * Get all WETH-like tokens from a transaction's trace */ async function findWethLikeTokens(trace) { try { const processedAddresses = new Set(); // Recursive function to process trace calls const processCalls = async (calls) => { for (const call of calls) { if (call.type === "CALL" && call.to && !processedAddresses.has(call.to)) { processedAddresses.add(call.to); // Check if this is a WETH-like token if (await isWethLikeToken(call.to)) { console.log(`Found WETH-like token: ${call.to}`); } } // Process nested calls if (call.calls && call.calls.length > 0) { await processCalls(call.calls); } } }; await processCalls(trace); } catch (error) { console.error(`Error finding WETH-like tokens: ${error}`); } } /** * Get event name from event topic and ABI * @param eventTopic The event topic (signature hash) * @param abi The contract ABI containing event definitions * @returns The event name or null if not found */ function getEventNameFromTopic(eventTopic, abi) { try { // Create an interface from the ABI const contractInterface = new ethers_1.ethers.Interface(abi); // Get the event fragment from the topic const eventFragment = contractInterface.getEvent(eventTopic); return eventFragment ? eventFragment.name : null; } catch (error) { console.error(`Error decoding event topic ${eventTopic}: ${error}`); return null; } } /** * Get event name and decode parameters from log data * @param log The raw log object with topics and data * @param abi The contract ABI containing event definitions * @returns Decoded event information or null if not found */ function decodeEventLog(log, abi) { try { if (!log.topics || log.topics.length === 0) { return null; } // Create an interface from the ABI const contractInterface = new ethers_1.ethers.Interface(abi); // The first topic is always the event signature const eventTopic = log.topics[0]; // Parse the log const parsedLog = contractInterface.parseLog({ topics: log.topics, data: log.data, }); if (parsedLog) { return { name: parsedLog.name, args: parsedLog.args, signature: parsedLog.signature, }; } return null; } catch (error) { console.error(`Error decoding event log: ${error}`); return null; } } /** * Generate event signature hash from event name and parameter types * @param eventName The name of the event * @param paramTypes Array of parameter types (e.g., ['address', 'uint256']) * @returns The keccak256 hash of the event signature */ function generateEventSignature(eventName, paramTypes) { const signature = `${eventName}(${paramTypes.join(",")})`; return ethers_1.ethers.id(signature); } /** * Fetch contract ABI from Etherscan API * @param contractAddress The contract address to get ABI for * @param apiKey Optional Etherscan API key (uses env var if not provided) * @returns The contract ABI as JSON array or null if not found/error */ async function getContractABI(contractAddress, apiKey) { try { const key = apiKey || exports.ETHERSCAN_API_KEY; if (!key) { console.warn("No Etherscan API key provided. Set ETHERSCAN_API_KEY environment variable."); return null; } const url = `https://api.etherscan.io/api?module=contract&action=getabi&address=${contractAddress}&apikey=${key}`; console.log(`Fetching ABI for contract: ${contractAddress}`); const response = await fetch(url); if (!response.ok) { console.error(`Etherscan API request failed: ${response.status} ${response.statusText}`); return null; } const data = await response.json(); if (data.status === "1" && data.result) { try { // Parse the ABI JSON string const abi = JSON.parse(data.result); console.log(`✅ Successfully fetched ABI for ${contractAddress} (${abi.length} items)`); return abi; } catch (parseError) { console.error(`Failed to parse ABI JSON: ${parseError}`); return null; } } else { console.error(`Etherscan API error: ${data.message || "Unknown error"}`); if (data.result === "Contract source code not verified") { console.warn("Contract source code is not verified on Etherscan"); } return null; } } catch (error) { console.error(`Error fetching ABI from Etherscan: ${error}`); return null; } } /** * Get contract ABI and decode event logs in one go * @param contractAddress The contract address * @param logs Array of raw logs from the contract * @param apiKey Optional Etherscan API key * @returns Array of decoded logs with event names and arguments */ /** * Check if an ABI contains proxy-related functions/events */ function isProxyABI(abi) { const proxyKeywords = [ "implementation", "upgrade", "proxy", "admin", "beacon", "fallback", "delegate", ]; // Check function and event names for proxy keywords const hasProxyElements = abi.some((item) => { if (item.type === "function" || item.type === "event") { const name = item.name?.toLowerCase() || ""; return proxyKeywords.some((keyword) => name.includes(keyword)); } return false; }); // Check if ABI is very minimal (common for proxy contracts) // const isMinimal = abi.filter((item) => item.type === "function").length < 5; return hasProxyElements; } async function getABIAndDecodeLog(contractAddress, log, apiKey) { let abi = await getContractABI(contractAddress, apiKey); // Check if we have an ABI but it's a proxy ABI if (abi && isProxyABI(abi)) { console.log(`ABI found for ${contractAddress} but appears to be a proxy, checking for implementation...`); try { const implementationAddress = await getImplementationAddress(contractAddress); if (implementationAddress) { console.log(`Found implementation contract: ${implementationAddress}`); const implementationABI = await getContractABI(implementationAddress, apiKey); if (implementationABI && !isProxyABI(implementationABI)) { console.log(`✅ Using implementation ABI from ${implementationAddress}`); abi = implementationABI; } else { console.log(`Implementation ABI not found or also a proxy, using original ABI`); } } } catch (error) { console.warn(`Failed to check for implementation contract: ${error}`); } } // If no ABI found at all, check if it's an upgradeable contract else if (!abi) { console.log(`No ABI found for ${contractAddress}, checking for implementation contract...`); try { const implementationAddress = await getImplementationAddress(contractAddress); if (implementationAddress) { console.log(`Found implementation contract: ${implementationAddress}`); abi = await getContractABI(implementationAddress, apiKey); if (abi) { console.log(`✅ Successfully fetched implementation ABI for ${implementationAddress}`); } } } catch (error) { console.warn(`Failed to check for implementation contract: ${error}`); } } if (!abi) { console.warn(`Could not fetch ABI for ${contractAddress} or its implementation, returning raw logs`); return { name: "Unknown", args: null, signature: log.topics?.[0] || "No topic", raw: log, }; } const decoded = decodeEventLog(log, abi); return { name: decoded?.name || "Unknown", args: decoded?.args || null, signature: decoded?.signature || log.topics?.[0] || "No topic", raw: log, }; }