flashloan-profit-calculator
Version:
A library for analyzing flashloan transactions and calculating profits
634 lines (633 loc) • 26.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTokenMetadata = exports.processTraceCall = exports.isBuilder = exports.updateBuilderAddress = exports.detectedFlashloanContracts = exports.blockTimestamp = exports.addressParticipation = exports.costBalanceChanges = exports.revenueBalanceChanges = exports.isFlashLoan = void 0;
exports.initializeBalanceChanges = initializeBalanceChanges;
exports.trackAddressParticipation = trackAddressParticipation;
exports.processTransfer = processTransfer;
exports.processTransferLog = processTransferLog;
exports.setupProcessor = setupProcessor;
exports.resetState = resetState;
exports.getTransactionDetails = getTransactionDetails;
const utils_1 = require("./utils");
const constants_1 = require("./constants");
const alchemy_sdk_1 = require("alchemy-sdk");
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
exports.isFlashLoan = false;
// Initialize Alchemy
const settings = {
apiKey: process.env.ALCHEMY_API_KEY,
network: alchemy_sdk_1.Network.ETH_MAINNET, // TODO: Make this configurable
};
const alchemy = new alchemy_sdk_1.Alchemy(settings);
// State management
exports.revenueBalanceChanges = {};
exports.costBalanceChanges = {};
exports.addressParticipation = {};
// Configuration variables
let builderAddress = "";
let contractAddress = "";
let senderAddress = "";
exports.blockTimestamp = 0;
// Global flashloan tracking
exports.detectedFlashloanContracts = [];
// Known event signatures
const TRANSFER_EVENT_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
const WITHDRAWAL_EVENT_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
const DEPOSIT_EVENT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
/**
* Update the builder address by fetching block information
*/
const updateBuilderAddress = async (blockNumber) => {
const block = await alchemy.core.getBlock(blockNumber);
builderAddress = block.miner;
exports.blockTimestamp = block.timestamp;
};
exports.updateBuilderAddress = updateBuilderAddress;
/**
* Check if an address is a builder
*/
const isBuilder = (address) => {
return address.toLowerCase() === builderAddress.toLowerCase();
};
exports.isBuilder = isBuilder;
/**
* Initialize balance changes for a token and address
*/
function initializeBalanceChanges(token, address, isRevenue, isSenderOrContract) {
if (address === "0x0") {
return;
}
const balanceChanges = isRevenue ? exports.revenueBalanceChanges : exports.costBalanceChanges;
if (!balanceChanges[token])
balanceChanges[token] = {};
if (!balanceChanges[token][address]) {
if (isSenderOrContract) {
balanceChanges[token][address] = {
amount: 0n,
type: isRevenue ? "Revenue" : "Cost",
};
}
else {
balanceChanges[token][address] = {
amount: 0n,
type: "TokenTransfer",
};
}
}
}
/**
* Track address participation in transfers
*/
function trackAddressParticipation(from, to, token) {
// Initialize address data if not exists
if (!exports.addressParticipation[from])
exports.addressParticipation[from] = {};
if (!exports.addressParticipation[to])
exports.addressParticipation[to] = {};
// Initialize token data for addresses if not exists
if (!exports.addressParticipation[from][token]) {
exports.addressParticipation[from][token] = {
toCount: 0,
fromCount: 0,
participation: 0,
};
}
if (!exports.addressParticipation[to][token]) {
exports.addressParticipation[to][token] = {
toCount: 0,
fromCount: 0,
participation: 0,
};
}
// Update counts
if (exports.addressParticipation[from][token].fromCount === 0) {
exports.addressParticipation[from][token].participation++;
}
exports.addressParticipation[from][token].fromCount++;
if (exports.addressParticipation[to][token].toCount === 0) {
exports.addressParticipation[to][token].participation++;
}
exports.addressParticipation[to][token].toCount++;
}
/**
* Process a transfer of tokens between addresses
*/
function processTransfer(token, from, to, amount) {
trackAddressParticipation(from, to, token);
// Initialize balance changes
const isFromSenderOrContract = from.toLowerCase() === senderAddress ||
from.toLowerCase() === contractAddress;
const isToSenderOrContract = to.toLowerCase() === senderAddress || to.toLowerCase() === contractAddress;
initializeBalanceChanges(token, to, true, isToSenderOrContract);
initializeBalanceChanges(token, from, false, isFromSenderOrContract);
// Update balances
if (to !== constants_1.NULL_ADDRESS) {
exports.revenueBalanceChanges[token][to].amount += amount;
}
if ((0, exports.isBuilder)(to)) {
exports.revenueBalanceChanges[token][to].amount *= -1n;
exports.revenueBalanceChanges[token][to].type = "Cost";
}
else {
if (from !== constants_1.NULL_ADDRESS) {
exports.costBalanceChanges[token][from].amount -= amount;
}
}
}
/**
* Process a transfer log with error handling
*/
function processTransferLog(log) {
try {
const token = log.raw.address.toLowerCase();
if (log.name === "Transfer") {
try {
const from = typeof log.inputs[0].value === "string"
? log.inputs[0].value.toLowerCase()
: log.inputs[0].value.toString().toLowerCase();
const to = typeof log.inputs[1].value === "string"
? log.inputs[1].value.toLowerCase()
: log.inputs[1].value.toString().toLowerCase();
const amount = BigInt(log.inputs[2].value);
console.log(`Transfer: ${from} -> ${to}, amount: ${amount.toString()}`);
processTransfer(token, from, to, amount);
}
catch (error) {
console.error(`Error processing Transfer log: ${error}`);
}
}
// Wrapped tokens deduct from the sender's balance during withdrawals but it's not logged
// So we need to manually deduct the cost from the sender's balance
if (log.name === "Withdrawal") {
try {
const from = typeof log.inputs[0].value === "string"
? log.inputs[0].value.toLowerCase()
: log.inputs[0].value.toString().toLowerCase();
const amount = BigInt(log.inputs[1].value);
console.log(`Withdrawal: ${from}, amount: ${amount.toString()}`);
// Treat it like a WETH transfer out
processTransfer(token, from, constants_1.NULL_ADDRESS, amount);
}
catch (error) {
console.error(`Error processing Withdrawal log: ${error}`);
}
}
// Wrapped tokens adds to the sender's balance during deposits but it's not logged
// So we need to manually add the revenue to the sender's balance
if (log.name === "Deposit") {
try {
const to = typeof log.inputs[0].value === "string"
? log.inputs[0].value.toLowerCase()
: log.inputs[0].value.toString().toLowerCase();
const amount = BigInt(log.inputs[1].value);
console.log(`Deposit: ${to}, amount: ${amount.toString()}`);
// Treat it like a WETH transfer in
processTransfer(token, constants_1.NULL_ADDRESS, to, amount);
}
catch (error) {
console.error(`Error processing Deposit log: ${error}`);
}
}
}
catch (error) {
console.error(`Failed to process log: ${error}`);
}
}
/**
* Process trace calls recursively to catch ETH transfers
*/
const processTraceCall = async (calls) => {
try {
if (!calls || calls.length === 0) {
console.log("No trace calls to process");
return;
}
let processedCalls = 0;
for (const call of calls) {
try {
// Handle ETH transfers
if (call.type === "CALL" && call.value && call.value !== "0x0") {
const token = constants_1.ETH_ADDRESS;
const from = call.from.toLowerCase();
const to = call.to.toLowerCase();
const amount = BigInt(call.value);
console.log(`ETH Transfer: ${from} -> ${to}, amount: ${amount.toString()}`);
processTransfer(token, from, to, amount);
processedCalls++;
}
// Handle ERC20 transfers and WETH-like operations
// else if (call.type === "CALL" && call.input) {
// const input = call.input.slice(0, 10); // Get function signature
// const token = call.to.toLowerCase();
// // Check if this is a WETH-like token
// const isWethLike = await isWethLikeToken(token);
// // transfer(address,uint256)
// if (input === "0xa9059cbb") {
// const [to, amount] = decodeTransferInput(call.input);
// const from = call.from.toLowerCase();
// console.log(
// `ERC20 Transfer: ${from} -> ${to}, token: ${token}, amount: ${amount.toString()}`
// );
// processTransfer(token, from, to, amount);
// processedCalls++;
// }
// // transferFrom(address,address,uint256)
// else if (input === "0x23b872dd") {
// const [from, to, amount] = decodeTransferFromInput(call.input);
// console.log(
// `ERC20 TransferFrom: ${from} -> ${to}, token: ${token}, amount: ${amount.toString()}`
// );
// processTransfer(token, from, to, amount);
// processedCalls++;
// }
// // deposit() - WETH-like
// else if (input === "0xd0e30db0" && isWethLike) {
// const amount = BigInt(call.value || "0");
// const from = call.from.toLowerCase();
// console.log(`WETH Deposit: ${from}, amount: ${amount.toString()}`);
// // Treat it like a WETH transfer in
// processTransfer(token, NULL_ADDRESS, from, amount);
// processedCalls++;
// }
// // withdraw(uint256) - WETH-like
// else if (input === "0x2e1a7d4d" && isWethLike) {
// const amount = decodeWithdrawInput(call.input);
// const from = call.from.toLowerCase();
// console.log(`WETH Withdraw: ${from}, amount: ${amount.toString()}`);
// // Treat it like a WETH transfer out
// processTransfer(token, from, NULL_ADDRESS, amount);
// processedCalls++;
// }
// }
if (call.calls && call.calls.length > 0) {
// Recursively process nested calls
await (0, exports.processTraceCall)(call.calls);
}
}
catch (error) {
console.error(`Error processing trace call: ${error}`);
}
}
}
catch (error) {
console.error(`Failed to process trace calls: ${error}`);
}
};
exports.processTraceCall = processTraceCall;
// Helper functions to decode ERC20 function inputs
function decodeTransferInput(input) {
const data = input.slice(10); // Remove function signature
const to = `0x${data.slice(24, 64)}`;
const amount = BigInt(`0x${data.slice(64, 128)}`);
return [to, amount];
}
function decodeTransferFromInput(input) {
const data = input.slice(10); // Remove function signature
const from = `0x${data.slice(24, 64)}`;
const to = `0x${data.slice(88, 128)}`;
const amount = BigInt(`0x${data.slice(128, 192)}`);
return [from, to, amount];
}
function decodeWithdrawInput(input) {
const data = input.slice(10); // Remove function signature
return BigInt(`0x${data.slice(24, 64)}`);
}
/**
* Setup the processor with contract and sender addresses
*/
function setupProcessor(contract, sender) {
contractAddress = contract.toLowerCase();
senderAddress = sender.toLowerCase();
}
const getTokenMetadata = async (contract) => {
if (contract === constants_1.ETH_ADDRESS) {
return {
name: "Ether",
symbol: "ETH",
decimals: 18,
logo: "https://cryptologos.cc/logos/ethereum-eth-logo.svg",
};
}
return await alchemy.core.getTokenMetadata(contract);
};
exports.getTokenMetadata = getTokenMetadata;
/**
* Reset all state
*/
function resetState() {
for (const key of Object.keys(exports.revenueBalanceChanges)) {
delete exports.revenueBalanceChanges[key];
}
for (const key of Object.keys(exports.costBalanceChanges)) {
delete exports.costBalanceChanges[key];
}
for (const key of Object.keys(exports.addressParticipation)) {
delete exports.addressParticipation[key];
}
// Reset flashloan tracking
exports.detectedFlashloanContracts.length = 0;
builderAddress = "";
contractAddress = "";
senderAddress = "";
}
/**
* Get transaction details from a transaction hash
*/
async function getTransactionDetails(txHash) {
try {
// Get transaction details
let transaction = null;
try {
transaction = await alchemy.core.getTransaction(txHash);
if (!transaction) {
throw new Error(`Transaction ${txHash} not found`);
}
}
catch (error) {
console.error(`Failed to get transaction: ${error}`);
throw error;
}
// Get transaction receipt for logs
let receipt = null;
try {
receipt = await alchemy.core.getTransactionReceipt(txHash);
if (!receipt) {
throw new Error(`Transaction receipt for ${txHash} not found`);
}
console.log(`Got receipt with ${receipt.logs.length} logs`);
}
catch (error) {
console.error(`Failed to get receipt: ${error}`);
throw error;
}
// Get block number
const blockNumber = receipt.blockNumber;
// Get sender address
const senderAddress = transaction.from.toLowerCase();
// Get contract address (the "to" address of the transaction)
const contractAddress = transaction.to?.toLowerCase() || "";
// Get trace data - add a default tracer
let trace = null;
try {
trace = await alchemy.debug.traceTransaction(txHash, {
type: alchemy_sdk_1.DebugTracerType.CALL_TRACER,
});
console.log(`Got trace data: ${JSON.stringify(trace).substring(0, 200)}...`);
}
catch (error) {
console.error(`Failed to get trace data: ${error}`);
console.error(`Error details: ${JSON.stringify(error)}`);
throw error;
}
// // Process trace calls safely
let traceCalls = [];
try {
if (Array.isArray(trace)) {
traceCalls = trace;
}
else if (typeof trace === "object" && trace !== null) {
const traceObj = trace;
if (Array.isArray(traceObj.calls)) {
traceCalls = traceObj.calls;
}
}
console.log(`Extracted ${traceCalls.length} trace calls`);
}
catch (error) {
console.error(`Failed to extract trace calls: ${error}`);
// Continue with empty trace calls
}
// Parse logs from receipt
let parsedLogs;
try {
parsedLogs = await parseLogsFromReceipt(receipt);
console.log(`Parsed ${parsedLogs.length} logs from receipt`);
}
catch (error) {
console.error(`Failed to parse logs: ${error}`);
throw error;
}
// Calculate gas cost in ETH
const gasUsed = BigInt(receipt.gasUsed.toString());
const effectiveGasPrice = BigInt(receipt.effectiveGasPrice?.toString() ||
transaction.gasPrice?.toString() ||
"0");
const gasCostWei = gasUsed * effectiveGasPrice;
const gasCostETH = Number(gasCostWei) / 1e18;
console.log(`Gas used: ${gasUsed.toString()}, Effective gas price: ${effectiveGasPrice.toString()}, Gas cost: ${gasCostETH.toFixed(6)} ETH`);
return {
blockNumber,
senderAddress,
contractAddress,
trace: traceCalls,
logs: parsedLogs,
gasCostETH,
gasCostWei: gasCostWei.toString(),
};
}
catch (error) {
console.error(`Failed to get transaction details: ${error}`);
console.error(`Stack trace: ${error.stack}`);
throw error;
}
}
/**
* Parse logs from transaction receipt without relying on external ABIs
*/
async function parseLogsFromReceipt(receipt) {
try {
const parsedLogs = [];
for (const log of receipt.logs) {
try {
// Debug log data
console.log(`Processing log: address=${log.address}, topics=${log.topics[0]?.substring(0, 10)}...`);
// Handle Transfer events (ERC20/ERC721)
if (log.topics[0] === TRANSFER_EVENT_TOPIC) {
try {
// Create a TransferLog object for the Transfer event
const transferLog = {
raw: {
address: log.address.toLowerCase(),
topics: log.topics,
data: log.data,
},
name: "Transfer",
anonymous: false,
inputs: [
{
name: "from",
value: padAddress(log.topics[1]),
type: "address",
indexed: true,
},
{
name: "to",
value: padAddress(log.topics[2]),
type: "address",
indexed: true,
},
{
name: "value",
value: parseAmountSafely(log.data),
type: "uint256",
indexed: false,
},
],
};
console.log(` Parsed Transfer: ${transferLog.inputs[0].value} -> ${transferLog.inputs[1].value}, amount: ${transferLog.inputs[2].value}`);
parsedLogs.push(transferLog);
}
catch (error) {
console.error(` Failed to parse Transfer event: ${error}`);
}
}
// Handle Withdrawal events (WETH)
else if (log.topics[0] === WITHDRAWAL_EVENT_TOPIC) {
try {
const withdrawalLog = {
raw: {
address: log.address.toLowerCase(),
topics: log.topics,
data: log.data,
},
name: "Withdrawal",
anonymous: false,
inputs: [
{
name: "src",
value: padAddress(log.topics[1]),
type: "address",
indexed: true,
},
{
name: "wad",
value: parseAmountSafely(log.data),
type: "uint256",
indexed: false,
},
],
};
console.log(` Parsed Withdrawal: from ${withdrawalLog.inputs[0].value}, amount: ${withdrawalLog.inputs[1].value}`);
parsedLogs.push(withdrawalLog);
}
catch (error) {
console.error(` Failed to parse Withdrawal event: ${error}`);
}
}
// Handle Deposit events (WETH)
else if (log.topics[0] === DEPOSIT_EVENT_TOPIC) {
try {
const depositLog = {
raw: {
address: log.address.toLowerCase(),
topics: log.topics,
data: log.data,
},
name: "Deposit",
anonymous: false,
inputs: [
{
name: "dst",
value: padAddress(log.topics[1]),
type: "address",
indexed: true,
},
{
name: "wad",
value: parseAmountSafely(log.data),
type: "uint256",
indexed: false,
},
],
};
console.log(` Parsed Deposit: to ${depositLog.inputs[0].value}, amount: ${depositLog.inputs[1].value}`);
parsedLogs.push(depositLog);
}
catch (error) {
console.error(` Failed to parse Deposit event: ${error}`);
}
}
// Flashloan detection
else {
try {
if (!exports.isFlashLoan) {
// Check if it's a known flashloan contract
const knownContract = constants_1.FLASHLOAN_CONTRACTS.find((contract) => contract.address.toLowerCase() === log.address.toLowerCase());
if (knownContract) {
console.log(`Known flashloan contract: ${knownContract.name}`);
exports.isFlashLoan = true;
if (!exports.detectedFlashloanContracts.includes(knownContract.name)) {
exports.detectedFlashloanContracts.push(knownContract.name);
}
return parsedLogs;
}
else {
// Try to decode event to check if it's a flashloan
const event = await (0, utils_1.getABIAndDecodeLog)(log.address, {
topics: log.topics,
data: log.data,
});
console.log(`Event decoded: ${event.name}`);
if (event.name === "FlashLoan") {
exports.isFlashLoan = true;
console.log(`Flashloan detected via event: ${event.name}`);
if (!exports.detectedFlashloanContracts.includes("Unknown")) {
exports.detectedFlashloanContracts.push("Unknown");
}
return parsedLogs;
}
}
}
}
catch (error) {
console.warn(`Failed to check flashloan for ${log.address}: ${error}`);
}
}
}
catch (error) {
// Skip logs that can't be parsed
console.warn(`Failed to process log: ${error}`);
}
}
return parsedLogs;
}
catch (error) {
console.error(`Fatal error in parseLogsFromReceipt: ${error}`);
throw error;
}
}
/**
* Helper to convert a topic to an address
*/
function padAddress(topic) {
if (!topic)
return "0x0";
return `0x${topic.substring(26).toLowerCase()}`;
}
/**
* Parse amount safely to handle scientific notation
*/
function parseAmountSafely(hexValue) {
try {
// Try to parse directly as BigInt first
return BigInt(`0x${hexValue.substring(2)}`).toString();
}
catch (error) {
try {
// If direct parsing fails, use Number.parseInt and handle scientific notation
const parsed = Number.parseInt(hexValue, 16);
if (Number.isFinite(parsed)) {
return parsed.toLocaleString("fullwide", { useGrouping: false });
}
throw new Error(`Invalid number: ${hexValue}`);
}
catch (err) {
console.error(`Failed to parse amount ${hexValue}: ${err}`);
return "0";
}
}
}