flashloan-profit-calculator
Version:
A library for analyzing flashloan transactions and calculating profits
322 lines (321 loc) • 12.5 kB
JavaScript
;
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,
};
}