Files
zeekayandHanzo Dev 64519916bf the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:01:04 -07:00

704 lines
23 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package evm
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/big"
"strings"
"time"
)
// Common DEX and lending protocol addresses
var (
// Uniswap V2 Router
UniswapV2Router = "0x7a250d5630b4cf539739df2c5dacb4c659f2488d"
// Uniswap V3 Router
UniswapV3Router = "0xe592427a0aece92de3edee1f18e0157c05861564"
// Sushiswap Router
SushiswapRouter = "0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f"
// Aave V2 Lending Pool
AaveV2LendingPool = "0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9"
// Aave V3 Pool
AaveV3Pool = "0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2"
// Compound V2 Comptroller
CompoundComptroller = "0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b"
// Event signatures
TopicSwap = "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822" // Swap(address,uint256,uint256,uint256,uint256,address)
TopicSwapV3 = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67" // Swap(address,address,int256,int256,uint160,uint128,int24)
TopicSync = "0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1" // Sync(uint112,uint112)
TopicLiquidation = "0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286" // LiquidationCall
)
// MEVIndexer handles MEV transaction detection and indexing
type MEVIndexer struct {
adapter *Adapter
db *sql.DB
}
// NewMEVIndexer creates a new MEV indexer
func NewMEVIndexer(adapter *Adapter, db *sql.DB) *MEVIndexer {
return &MEVIndexer{
adapter: adapter,
db: db,
}
}
// InitSchema creates MEV database tables
func (m *MEVIndexer) InitSchema() error {
schema := `
-- MEV transactions table
CREATE TABLE IF NOT EXISTS mev_transactions (
id TEXT PRIMARY KEY,
transaction_hash TEXT NOT NULL UNIQUE,
block_number BIGINT NOT NULL,
mev_type TEXT NOT NULL,
extractor_address TEXT NOT NULL,
victim_address TEXT,
protocol TEXT,
profit_eth TEXT NOT NULL DEFAULT '0',
profit_usd TEXT,
gas_cost_eth TEXT NOT NULL DEFAULT '0',
related_tx_hashes JSONB DEFAULT '[]',
confidence FLOAT DEFAULT 0,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mev_tx_block ON mev_transactions(block_number);
CREATE INDEX IF NOT EXISTS idx_mev_tx_type ON mev_transactions(mev_type);
CREATE INDEX IF NOT EXISTS idx_mev_tx_extractor ON mev_transactions(extractor_address);
CREATE INDEX IF NOT EXISTS idx_mev_tx_victim ON mev_transactions(victim_address) WHERE victim_address IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_mev_tx_timestamp ON mev_transactions(timestamp);
CREATE INDEX IF NOT EXISTS idx_mev_tx_profit ON mev_transactions((CAST(profit_eth AS NUMERIC)) DESC);
-- Sandwich attacks table
CREATE TABLE IF NOT EXISTS mev_sandwiches (
id TEXT PRIMARY KEY,
block_number BIGINT NOT NULL,
frontrun_tx_hash TEXT NOT NULL,
victim_tx_hash TEXT NOT NULL,
backrun_tx_hash TEXT NOT NULL,
attacker_address TEXT NOT NULL,
victim_address TEXT NOT NULL,
token_address TEXT NOT NULL,
pool_address TEXT NOT NULL,
victim_loss_eth TEXT NOT NULL DEFAULT '0',
attacker_profit_eth TEXT NOT NULL DEFAULT '0',
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mev_sandwich_block ON mev_sandwiches(block_number);
CREATE INDEX IF NOT EXISTS idx_mev_sandwich_attacker ON mev_sandwiches(attacker_address);
CREATE INDEX IF NOT EXISTS idx_mev_sandwich_victim ON mev_sandwiches(victim_address);
CREATE INDEX IF NOT EXISTS idx_mev_sandwich_pool ON mev_sandwiches(pool_address);
-- Arbitrage transactions table
CREATE TABLE IF NOT EXISTS mev_arbitrages (
id TEXT PRIMARY KEY,
transaction_hash TEXT NOT NULL UNIQUE,
block_number BIGINT NOT NULL,
arbitrager_address TEXT NOT NULL,
profit_eth TEXT NOT NULL DEFAULT '0',
profit_usd TEXT,
path_length INT DEFAULT 0,
tokens JSONB DEFAULT '[]',
pools JSONB DEFAULT '[]',
is_atomic BOOLEAN DEFAULT FALSE,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mev_arb_block ON mev_arbitrages(block_number);
CREATE INDEX IF NOT EXISTS idx_mev_arb_address ON mev_arbitrages(arbitrager_address);
CREATE INDEX IF NOT EXISTS idx_mev_arb_profit ON mev_arbitrages((CAST(profit_eth AS NUMERIC)) DESC);
-- Liquidations table
CREATE TABLE IF NOT EXISTS mev_liquidations (
id TEXT PRIMARY KEY,
transaction_hash TEXT NOT NULL UNIQUE,
block_number BIGINT NOT NULL,
liquidator_address TEXT NOT NULL,
borrower_address TEXT NOT NULL,
protocol TEXT NOT NULL,
collateral_token TEXT NOT NULL,
debt_token TEXT NOT NULL,
collateral_seized TEXT NOT NULL DEFAULT '0',
debt_repaid TEXT NOT NULL DEFAULT '0',
liquidator_profit_eth TEXT NOT NULL DEFAULT '0',
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mev_liq_block ON mev_liquidations(block_number);
CREATE INDEX IF NOT EXISTS idx_mev_liq_liquidator ON mev_liquidations(liquidator_address);
CREATE INDEX IF NOT EXISTS idx_mev_liq_borrower ON mev_liquidations(borrower_address);
CREATE INDEX IF NOT EXISTS idx_mev_liq_protocol ON mev_liquidations(protocol);
-- Private/Flashbots transactions table
CREATE TABLE IF NOT EXISTS mev_private_txs (
transaction_hash TEXT PRIMARY KEY,
block_number BIGINT NOT NULL,
tx_from TEXT NOT NULL,
tx_to TEXT,
value TEXT,
gas_price TEXT,
is_flashbots BOOLEAN DEFAULT FALSE,
bundle_index INT,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mev_private_block ON mev_private_txs(block_number);
CREATE INDEX IF NOT EXISTS idx_mev_private_from ON mev_private_txs(tx_from);
-- MEV stats table
CREATE TABLE IF NOT EXISTS mev_stats (
id INT PRIMARY KEY DEFAULT 1,
total_mev_transactions BIGINT DEFAULT 0,
total_sandwiches BIGINT DEFAULT 0,
total_arbitrages BIGINT DEFAULT 0,
total_liquidations BIGINT DEFAULT 0,
total_private_txs BIGINT DEFAULT 0,
total_profit_eth TEXT DEFAULT '0',
total_victim_loss_eth TEXT DEFAULT '0',
updated_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO mev_stats (id) VALUES (1) ON CONFLICT DO NOTHING;
`
_, err := m.db.Exec(schema)
return err
}
// ProcessBlock analyzes a block for MEV activity
func (m *MEVIndexer) ProcessBlock(ctx context.Context, blockNumber uint64) error {
// Get block with transactions
blockData, err := m.adapter.GetBlockByHeight(ctx, blockNumber)
if err != nil {
return err
}
var block struct {
Hash string `json:"hash"`
Timestamp string `json:"timestamp"`
Transactions []struct {
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Input string `json:"input"`
} `json:"transactions"`
}
if err := json.Unmarshal(blockData, &block); err != nil {
return err
}
timestamp := time.Unix(int64(hexToUint64(block.Timestamp)), 0)
// Analyze transactions for MEV patterns
for i, tx := range block.Transactions {
// Check for sandwich patterns
if i > 0 && i < len(block.Transactions)-1 {
m.detectSandwich(ctx, block.Transactions[i-1].Hash, tx.Hash, block.Transactions[i+1].Hash, blockNumber, timestamp)
}
// Check for arbitrage
m.detectArbitrage(ctx, tx.Hash, blockNumber, timestamp)
// Check for liquidations
m.detectLiquidation(ctx, tx.Hash, blockNumber, timestamp)
// Check for private transactions (simple heuristic)
m.detectPrivateTx(ctx, tx.Hash, tx.From, tx.To, tx.Value, tx.GasPrice, blockNumber, timestamp)
}
return nil
}
// detectSandwich detects potential sandwich attacks
func (m *MEVIndexer) detectSandwich(ctx context.Context, tx1Hash, tx2Hash, tx3Hash string, blockNumber uint64, timestamp time.Time) {
// Get transaction receipts
tx1, logs1, err := m.adapter.GetTransactionReceipt(ctx, tx1Hash)
if err != nil || tx1.Status == nil || *tx1.Status != 1 {
return
}
tx2, logs2, err := m.adapter.GetTransactionReceipt(ctx, tx2Hash)
if err != nil || tx2.Status == nil || *tx2.Status != 1 {
return
}
tx3, logs3, err := m.adapter.GetTransactionReceipt(ctx, tx3Hash)
if err != nil || tx3.Status == nil || *tx3.Status != 1 {
return
}
// Check if tx1 and tx3 are from the same address (potential attacker)
if tx1.From != tx3.From {
return
}
// Check for swap events in all three transactions
swap1 := findSwapEvent(logs1)
swap2 := findSwapEvent(logs2)
swap3 := findSwapEvent(logs3)
if swap1 == nil || swap2 == nil || swap3 == nil {
return
}
// Check if all swaps are on the same pool
if swap1.Address != swap2.Address || swap2.Address != swap3.Address {
return
}
// This looks like a sandwich attack
sandwich := SandwichAttack{
ID: fmt.Sprintf("sandwich-%d-%s", blockNumber, tx2Hash),
BlockNumber: blockNumber,
FrontrunTxHash: tx1Hash,
VictimTxHash: tx2Hash,
BackrunTxHash: tx3Hash,
AttackerAddress: tx1.From,
VictimAddress: tx2.From,
PoolAddress: swap1.Address,
Timestamp: timestamp,
}
// Calculate profit and loss (simplified)
sandwich.AttackerProfitETH = "0" // Would need price oracle for accurate calculation
sandwich.VictimLossETH = "0"
m.StoreSandwich(ctx, sandwich)
// Also store as MEV transaction
m.StoreMEVTransaction(ctx, MEVTransaction{
ID: sandwich.ID,
TransactionHash: tx3Hash,
BlockNumber: blockNumber,
MEVType: MEVTypeSandwich,
ExtractorAddress: tx1.From,
VictimAddress: tx2.From,
Protocol: detectProtocol(swap1.Address),
ProfitETH: sandwich.AttackerProfitETH,
GasCostETH: calculateGasCost(tx1, tx3),
RelatedTxHashes: []string{tx1Hash, tx2Hash, tx3Hash},
Timestamp: timestamp,
Confidence: 0.8,
})
}
// detectArbitrage detects arbitrage transactions
func (m *MEVIndexer) detectArbitrage(ctx context.Context, txHash string, blockNumber uint64, timestamp time.Time) {
tx, logs, err := m.adapter.GetTransactionReceipt(ctx, txHash)
if err != nil || tx.Status == nil || *tx.Status != 1 {
return
}
// Count swap events
var swaps []Log
for _, log := range logs {
if len(log.Topics) > 0 && (log.Topics[0] == TopicSwap || log.Topics[0] == TopicSwapV3) {
swaps = append(swaps, log)
}
}
// Arbitrage typically has 2+ swaps
if len(swaps) < 2 {
return
}
// Check for circular arbitrage (start and end with same token)
// This is a simplified detection - real detection would track token flows
var tokens []string
var pools []string
for _, swap := range swaps {
pools = append(pools, swap.Address)
// Would extract tokens from swap data
}
// Check if it's atomic (uses flash loan)
isAtomic := detectFlashLoan(logs)
// Check if the transaction starts and ends with the same ETH balance change
// (circular arbitrage typically returns to starting token)
arb := ArbitrageTx{
ID: fmt.Sprintf("arb-%d-%s", blockNumber, txHash),
TransactionHash: txHash,
BlockNumber: blockNumber,
ArbitragerAddress: tx.From,
PathLength: len(swaps),
Tokens: tokens,
Pools: pools,
IsAtomic: isAtomic,
Timestamp: timestamp,
}
// Calculate profit (simplified)
arb.ProfitETH = "0" // Would need actual calculation
// Only store if confidence is high enough
if len(swaps) >= 3 || isAtomic {
m.StoreArbitrage(ctx, arb)
m.StoreMEVTransaction(ctx, MEVTransaction{
ID: arb.ID,
TransactionHash: txHash,
BlockNumber: blockNumber,
MEVType: MEVTypeArbitrage,
ExtractorAddress: tx.From,
ProfitETH: arb.ProfitETH,
GasCostETH: calculateGasCostSingle(tx),
Timestamp: timestamp,
Confidence: 0.7,
})
}
}
// detectLiquidation detects liquidation transactions
func (m *MEVIndexer) detectLiquidation(ctx context.Context, txHash string, blockNumber uint64, timestamp time.Time) {
tx, logs, err := m.adapter.GetTransactionReceipt(ctx, txHash)
if err != nil || tx.Status == nil || *tx.Status != 1 {
return
}
// Look for liquidation events
for _, log := range logs {
if len(log.Topics) == 0 {
continue
}
// Check for Aave/Compound liquidation events
if log.Topics[0] == TopicLiquidation {
liq := LiquidationTx{
ID: fmt.Sprintf("liq-%d-%s", blockNumber, txHash),
TransactionHash: txHash,
BlockNumber: blockNumber,
LiquidatorAddress: tx.From,
Protocol: detectLendingProtocol(log.Address),
Timestamp: timestamp,
}
// Parse liquidation event data
if len(log.Topics) >= 4 {
liq.CollateralToken = topicToAddress(log.Topics[1])
liq.DebtToken = topicToAddress(log.Topics[2])
liq.BorrowerAddress = topicToAddress(log.Topics[3])
}
// Parse amounts from data
if len(log.Data) >= 130 {
data := strings.TrimPrefix(log.Data, "0x")
liq.DebtRepaid = hexToBigInt("0x" + data[0:64]).String()
liq.CollateralSeized = hexToBigInt("0x" + data[64:128]).String()
}
m.StoreLiquidation(ctx, liq)
m.StoreMEVTransaction(ctx, MEVTransaction{
ID: liq.ID,
TransactionHash: txHash,
BlockNumber: blockNumber,
MEVType: MEVTypeLiquidation,
ExtractorAddress: tx.From,
VictimAddress: liq.BorrowerAddress,
Protocol: liq.Protocol,
ProfitETH: liq.LiquidatorProfitETH,
GasCostETH: calculateGasCostSingle(tx),
Timestamp: timestamp,
Confidence: 0.95,
})
break
}
}
}
// detectPrivateTx detects private/Flashbots transactions
func (m *MEVIndexer) detectPrivateTx(ctx context.Context, txHash, from, to, value, gasPrice string, blockNumber uint64, timestamp time.Time) {
// Simple heuristic: very high gas price might indicate private tx
gasPriceBI := hexToBigInt(gasPrice)
// If gas price is significantly above base fee, might be private
// This is a simplified heuristic - real detection would compare to block base fee
threshold := new(big.Int)
threshold.SetString("100000000000", 10) // 100 gwei
if gasPriceBI.Cmp(threshold) > 0 {
m.db.ExecContext(ctx, `
INSERT INTO mev_private_txs (transaction_hash, block_number, tx_from, tx_to, value, gas_price, is_flashbots, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (transaction_hash) DO NOTHING
`, txHash, blockNumber, from, to, value, gasPrice, false, timestamp)
}
}
// StoreMEVTransaction stores an MEV transaction
func (m *MEVIndexer) StoreMEVTransaction(ctx context.Context, tx MEVTransaction) error {
relatedJSON, _ := json.Marshal(tx.RelatedTxHashes)
_, err := m.db.ExecContext(ctx, `
INSERT INTO mev_transactions
(id, transaction_hash, block_number, mev_type, extractor_address, victim_address,
protocol, profit_eth, profit_usd, gas_cost_eth, related_tx_hashes, confidence, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (id) DO UPDATE SET
confidence = GREATEST(mev_transactions.confidence, EXCLUDED.confidence)
`, tx.ID, tx.TransactionHash, tx.BlockNumber, tx.MEVType, tx.ExtractorAddress,
tx.VictimAddress, tx.Protocol, tx.ProfitETH, tx.ProfitUSD, tx.GasCostETH,
relatedJSON, tx.Confidence, tx.Timestamp)
return err
}
// StoreSandwich stores a sandwich attack
func (m *MEVIndexer) StoreSandwich(ctx context.Context, s SandwichAttack) error {
_, err := m.db.ExecContext(ctx, `
INSERT INTO mev_sandwiches
(id, block_number, frontrun_tx_hash, victim_tx_hash, backrun_tx_hash,
attacker_address, victim_address, token_address, pool_address,
victim_loss_eth, attacker_profit_eth, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (id) DO NOTHING
`, s.ID, s.BlockNumber, s.FrontrunTxHash, s.VictimTxHash, s.BackrunTxHash,
s.AttackerAddress, s.VictimAddress, s.TokenAddress, s.PoolAddress,
s.VictimLossETH, s.AttackerProfitETH, s.Timestamp)
return err
}
// StoreArbitrage stores an arbitrage transaction
func (m *MEVIndexer) StoreArbitrage(ctx context.Context, a ArbitrageTx) error {
tokensJSON, _ := json.Marshal(a.Tokens)
poolsJSON, _ := json.Marshal(a.Pools)
_, err := m.db.ExecContext(ctx, `
INSERT INTO mev_arbitrages
(id, transaction_hash, block_number, arbitrager_address, profit_eth, profit_usd,
path_length, tokens, pools, is_atomic, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id) DO NOTHING
`, a.ID, a.TransactionHash, a.BlockNumber, a.ArbitragerAddress, a.ProfitETH,
a.ProfitUSD, a.PathLength, tokensJSON, poolsJSON, a.IsAtomic, a.Timestamp)
return err
}
// StoreLiquidation stores a liquidation transaction
func (m *MEVIndexer) StoreLiquidation(ctx context.Context, l LiquidationTx) error {
_, err := m.db.ExecContext(ctx, `
INSERT INTO mev_liquidations
(id, transaction_hash, block_number, liquidator_address, borrower_address,
protocol, collateral_token, debt_token, collateral_seized, debt_repaid,
liquidator_profit_eth, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (id) DO NOTHING
`, l.ID, l.TransactionHash, l.BlockNumber, l.LiquidatorAddress, l.BorrowerAddress,
l.Protocol, l.CollateralToken, l.DebtToken, l.CollateralSeized, l.DebtRepaid,
l.LiquidatorProfitETH, l.Timestamp)
return err
}
// GetMEVTransactions retrieves MEV transactions with filters
func (m *MEVIndexer) GetMEVTransactions(ctx context.Context, mevType string, limit, offset int) ([]MEVTransaction, error) {
query := `
SELECT id, transaction_hash, block_number, mev_type, extractor_address, victim_address,
protocol, profit_eth, profit_usd, gas_cost_eth, related_tx_hashes, confidence, timestamp
FROM mev_transactions
`
var args []interface{}
argCount := 0
if mevType != "" {
argCount++
query += fmt.Sprintf(" WHERE mev_type = $%d", argCount)
args = append(args, mevType)
}
query += " ORDER BY block_number DESC, timestamp DESC"
argCount++
query += fmt.Sprintf(" LIMIT $%d", argCount)
args = append(args, limit)
argCount++
query += fmt.Sprintf(" OFFSET $%d", argCount)
args = append(args, offset)
rows, err := m.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var txs []MEVTransaction
for rows.Next() {
var tx MEVTransaction
var victim, protocol, profitUSD sql.NullString
var relatedJSON []byte
err := rows.Scan(
&tx.ID, &tx.TransactionHash, &tx.BlockNumber, &tx.MEVType, &tx.ExtractorAddress,
&victim, &protocol, &tx.ProfitETH, &profitUSD, &tx.GasCostETH,
&relatedJSON, &tx.Confidence, &tx.Timestamp,
)
if err != nil {
continue
}
if victim.Valid {
tx.VictimAddress = victim.String
}
if protocol.Valid {
tx.Protocol = protocol.String
}
if profitUSD.Valid {
tx.ProfitUSD = profitUSD.String
}
if len(relatedJSON) > 0 {
json.Unmarshal(relatedJSON, &tx.RelatedTxHashes)
}
txs = append(txs, tx)
}
return txs, nil
}
// GetSandwiches retrieves sandwich attacks
func (m *MEVIndexer) GetSandwiches(ctx context.Context, limit, offset int) ([]SandwichAttack, error) {
rows, err := m.db.QueryContext(ctx, `
SELECT id, block_number, frontrun_tx_hash, victim_tx_hash, backrun_tx_hash,
attacker_address, victim_address, token_address, pool_address,
victim_loss_eth, attacker_profit_eth, timestamp
FROM mev_sandwiches
ORDER BY block_number DESC
LIMIT $1 OFFSET $2
`, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var sandwiches []SandwichAttack
for rows.Next() {
var s SandwichAttack
err := rows.Scan(
&s.ID, &s.BlockNumber, &s.FrontrunTxHash, &s.VictimTxHash, &s.BackrunTxHash,
&s.AttackerAddress, &s.VictimAddress, &s.TokenAddress, &s.PoolAddress,
&s.VictimLossETH, &s.AttackerProfitETH, &s.Timestamp,
)
if err != nil {
continue
}
sandwiches = append(sandwiches, s)
}
return sandwiches, nil
}
// UpdateStats updates MEV statistics
func (m *MEVIndexer) UpdateStats(ctx context.Context) error {
_, err := m.db.ExecContext(ctx, `
UPDATE mev_stats SET
total_mev_transactions = (SELECT COUNT(*) FROM mev_transactions),
total_sandwiches = (SELECT COUNT(*) FROM mev_sandwiches),
total_arbitrages = (SELECT COUNT(*) FROM mev_arbitrages),
total_liquidations = (SELECT COUNT(*) FROM mev_liquidations),
total_private_txs = (SELECT COUNT(*) FROM mev_private_txs),
total_profit_eth = COALESCE((SELECT SUM(CAST(profit_eth AS NUMERIC)) FROM mev_transactions), 0)::TEXT,
total_victim_loss_eth = COALESCE((SELECT SUM(CAST(victim_loss_eth AS NUMERIC)) FROM mev_sandwiches), 0)::TEXT,
updated_at = NOW()
WHERE id = 1
`)
return err
}
// GetStats retrieves MEV statistics
func (m *MEVIndexer) GetStats(ctx context.Context) (map[string]interface{}, error) {
var totalTxs, totalSandwiches, totalArbs, totalLiqs, totalPrivate int64
var totalProfit, totalVictimLoss string
var updatedAt time.Time
err := m.db.QueryRowContext(ctx, `
SELECT total_mev_transactions, total_sandwiches, total_arbitrages, total_liquidations,
total_private_txs, total_profit_eth, total_victim_loss_eth, updated_at
FROM mev_stats WHERE id = 1
`).Scan(&totalTxs, &totalSandwiches, &totalArbs, &totalLiqs, &totalPrivate,
&totalProfit, &totalVictimLoss, &updatedAt)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
return map[string]interface{}{
"total_mev_transactions": totalTxs,
"total_sandwiches": totalSandwiches,
"total_arbitrages": totalArbs,
"total_liquidations": totalLiqs,
"total_private_txs": totalPrivate,
"total_profit_eth": totalProfit,
"total_victim_loss_eth": totalVictimLoss,
"updated_at": updatedAt,
}, nil
}
// Helper functions
func findSwapEvent(logs []Log) *Log {
for _, log := range logs {
if len(log.Topics) > 0 && (log.Topics[0] == TopicSwap || log.Topics[0] == TopicSwapV3) {
return &log
}
}
return nil
}
func detectFlashLoan(logs []Log) bool {
// Look for flash loan signatures
flashLoanTopics := []string{
"0x631042c832b07452973831137f2d73e395028b44b250dedc5abb0ee766e168ac", // Aave FlashLoan
"0x5c29b9b7b0c0d3b0f9f5c0f0c0d3b0f9f5c0f0c0d3b0f9f5c0f0c0d3b0f9f5c0", // Balancer FlashLoan
}
for _, log := range logs {
if len(log.Topics) > 0 {
for _, topic := range flashLoanTopics {
if log.Topics[0] == topic {
return true
}
}
}
}
return false
}
func detectProtocol(poolAddress string) string {
// Would have a mapping of known pool addresses to protocols
// Simplified version
return "Unknown DEX"
}
func detectLendingProtocol(address string) string {
addr := strings.ToLower(address)
if addr == AaveV2LendingPool || addr == AaveV3Pool {
return "Aave"
}
if addr == CompoundComptroller {
return "Compound"
}
return "Unknown"
}
func calculateGasCost(tx1, tx3 *Transaction) string {
gas1 := new(big.Int).SetUint64(tx1.GasUsed)
gas3 := new(big.Int).SetUint64(tx3.GasUsed)
gasPrice := hexToBigInt(tx1.GasPrice)
totalGas := new(big.Int).Add(gas1, gas3)
cost := new(big.Int).Mul(totalGas, gasPrice)
return cost.String()
}
func calculateGasCostSingle(tx *Transaction) string {
gas := new(big.Int).SetUint64(tx.GasUsed)
gasPrice := hexToBigInt(tx.GasPrice)
cost := new(big.Int).Mul(gas, gasPrice)
return cost.String()
}