fix(multicall): resolve critical multicall parsing corruption issues

- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Krypto Kajun
2025-10-17 00:12:55 -05:00
parent f358f49aa9
commit 850223a953
8621 changed files with 79808 additions and 7340 deletions

View File

@@ -13,11 +13,31 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/internal/utils"
"github.com/fraktal/mev-beta/internal/validation"
"github.com/fraktal/mev-beta/pkg/calldata"
"github.com/fraktal/mev-beta/pkg/transport"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)
// safeConvertUint64ToInt64 safely converts a uint64 to int64, capping at MaxInt64 if overflow would occur
func safeConvertUint64ToInt64(v uint64) int64 {
if v > math.MaxInt64 {
return math.MaxInt64
}
return int64(v)
}
// safeConvertUint64ToInt safely converts a uint64 to int, capping at MaxInt32 if overflow would occur
func safeConvertUint64ToInt(v uint64) int {
if v > math.MaxInt32 {
return math.MaxInt32
}
return int(v)
}
// MarketDiscovery interface defines the methods needed from market discovery
type MarketDiscovery interface {
GetPoolCache() interface{} // Will return *arbitrum.PoolCache but using interface{} to prevent import cycle
@@ -171,16 +191,20 @@ func NewEnhancedSequencerParser(providerManager *transport.ProviderManager, logg
func NewABIDecoder() (ABIDecoder, error) {
// Return a placeholder implementation
decoder := &sophisticatedABIDecoder{
protocolABIs: make(map[string]*abi.ABI),
logger: nil,
protocolABIs: make(map[string]*abi.ABI),
logger: nil,
addressValidator: validation.NewAddressValidator(),
safeConverter: utils.NewSafeAddressConverter(),
}
return decoder, nil
}
// sophisticatedABIDecoder implements comprehensive ABI decoding for all DEX protocols
type sophisticatedABIDecoder struct {
protocolABIs map[string]*abi.ABI
logger *logger.Logger
protocolABIs map[string]*abi.ABI
logger *logger.Logger
addressValidator *validation.AddressValidator
safeConverter *utils.SafeAddressConverter
}
func (p *sophisticatedABIDecoder) DecodeSwapTransaction(protocol string, data []byte) (interface{}, error) {
@@ -219,6 +243,9 @@ func (p *sophisticatedABIDecoder) decodeUniswapV3Swap(methodSig []byte, data []b
// exactOutput: 0xf28c0498
switch {
case bytes.Equal(methodSig, []byte{0xac, 0x96, 0x50, 0xd8}), // multicall(uint256,bytes[])
bytes.Equal(methodSig, []byte{0x5a, 0xe4, 0x01, 0xdc}): // multicall(bytes[])
return p.decodeUniswapV3Multicall(data)
case bytes.Equal(methodSig, []byte{0x41, 0x4b, 0xf3, 0x89}): // exactInputSingle
return p.decodeExactInputSingle(data)
case bytes.Equal(methodSig, []byte{0xdb, 0x3e, 0x21, 0x98}): // exactOutputSingle
@@ -232,20 +259,56 @@ func (p *sophisticatedABIDecoder) decodeUniswapV3Swap(methodSig []byte, data []b
}
}
// decodeExactInputSingle decodes exactInputSingle parameters
func (p *sophisticatedABIDecoder) decodeExactInputSingle(data []byte) (*SwapEvent, error) {
if len(data) < 224 { // 7 * 32 bytes for the struct
return nil, fmt.Errorf("data too short for exactInputSingle")
func (p *sophisticatedABIDecoder) parseUniswapV3SingleSwapParams(data []byte) (tokenIn, tokenOut, recipient common.Address, fee, deadline, amountA, amountB *big.Int, err error) {
if len(data) < 224 {
err = fmt.Errorf("data too short for single swap")
return
}
// Parse ExactInputSingleParams struct
tokenIn := common.BytesToAddress(data[12:32])
tokenOut := common.BytesToAddress(data[44:64])
fee := new(big.Int).SetBytes(data[64:96])
recipient := common.BytesToAddress(data[108:128])
deadline := new(big.Int).SetBytes(data[128:160])
amountIn := new(big.Int).SetBytes(data[160:192])
amountOutMinimum := new(big.Int).SetBytes(data[192:224])
// PHASE 5 FIX: Use safe address conversion for token extraction
rawTokenIn := common.BytesToAddress(data[12:32])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawTokenIn.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
err = fmt.Errorf("invalid tokenIn address: %s (corruption score: %d)", rawTokenIn.Hex(), validation.CorruptionScore)
return
}
}
tokenIn = rawTokenIn
rawTokenOut := common.BytesToAddress(data[44:64])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawTokenOut.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
err = fmt.Errorf("invalid tokenOut address: %s (corruption score: %d)", rawTokenOut.Hex(), validation.CorruptionScore)
return
}
}
tokenOut = rawTokenOut
rawRecipient := common.BytesToAddress(data[108:128])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawRecipient.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
err = fmt.Errorf("invalid recipient address: %s (corruption score: %d)", rawRecipient.Hex(), validation.CorruptionScore)
return
}
}
recipient = rawRecipient
fee = new(big.Int).SetBytes(data[64:96])
deadline = new(big.Int).SetBytes(data[128:160])
amountA = new(big.Int).SetBytes(data[160:192])
amountB = new(big.Int).SetBytes(data[192:224])
return
}
// decodeExactInputSingle decodes exactInputSingle parameters
func (p *sophisticatedABIDecoder) decodeExactInputSingle(data []byte) (*SwapEvent, error) {
tokenIn, tokenOut, recipient, fee, deadline, amountIn, amountOutMinimum, err := p.parseUniswapV3SingleSwapParams(data)
if err != nil {
return nil, fmt.Errorf("error parsing exactInputSingle: %w", err)
}
return &SwapEvent{
Timestamp: time.Now(),
@@ -278,8 +341,26 @@ func (p *sophisticatedABIDecoder) decodeUniswapV2Swap(methodSig []byte, data []b
}
func (p *sophisticatedABIDecoder) CalculatePoolAddress(protocol, tokenA, tokenB string, fee interface{}) (common.Address, error) {
tokenAAddr := common.HexToAddress(tokenA)
tokenBAddr := common.HexToAddress(tokenB)
// PHASE 5 FIX: Use safe address conversion for pool calculation
var tokenAAddr, tokenBAddr common.Address
if p.safeConverter != nil {
result := p.safeConverter.SafeHexToAddress(tokenA)
if !result.IsValid {
return common.Address{}, fmt.Errorf("invalid tokenA address: %s (%v)", tokenA, result.Error)
}
tokenAAddr = result.Address
result = p.safeConverter.SafeHexToAddress(tokenB)
if !result.IsValid {
return common.Address{}, fmt.Errorf("invalid tokenB address: %s (%v)", tokenB, result.Error)
}
tokenBAddr = result.Address
} else {
// Fallback to unsafe conversion if safe converter not available
tokenAAddr = common.HexToAddress(tokenA)
tokenBAddr = common.HexToAddress(tokenB)
}
// Ensure token ordering (token0 < token1)
if bytes.Compare(tokenAAddr.Bytes(), tokenBAddr.Bytes()) > 0 {
@@ -361,7 +442,11 @@ func (p *EnhancedSequencerParser) ParseBlockForMEV(ctx context.Context, blockNum
}
// Update timestamp with actual block time
opportunities.Timestamp = time.Unix(int64(block.Time()), 0)
blockTime := block.Time()
if blockTime > math.MaxInt64 {
return nil, fmt.Errorf("block timestamp %d exceeds maximum int64 value", blockTime)
}
opportunities.Timestamp = time.Unix(safeConvertUint64ToInt64(blockTime), 0)
// Process all transactions in the block
for _, tx := range block.Transactions() {
@@ -456,18 +541,11 @@ func (p *EnhancedSequencerParser) calculateROI(profit, investment *big.Int) floa
// decodeExactOutputSingle decodes exactOutputSingle parameters
func (p *sophisticatedABIDecoder) decodeExactOutputSingle(data []byte) (*SwapEvent, error) {
if len(data) < 224 {
return nil, fmt.Errorf("data too short for exactOutputSingle")
tokenIn, tokenOut, recipient, fee, deadline, amountOut, amountInMaximum, err := p.parseUniswapV3SingleSwapParams(data)
if err != nil {
return nil, fmt.Errorf("error parsing exactOutputSingle: %w", err)
}
tokenIn := common.BytesToAddress(data[12:32])
tokenOut := common.BytesToAddress(data[44:64])
fee := new(big.Int).SetBytes(data[64:96])
recipient := common.BytesToAddress(data[108:128])
deadline := new(big.Int).SetBytes(data[128:160])
amountOut := new(big.Int).SetBytes(data[160:192])
amountInMaximum := new(big.Int).SetBytes(data[192:224])
return &SwapEvent{
Timestamp: time.Now(),
Protocol: "uniswap_v3",
@@ -488,18 +566,35 @@ func (p *sophisticatedABIDecoder) decodeExactInput(data []byte) (*SwapEvent, err
return nil, fmt.Errorf("data too short for exactInput")
}
recipient := common.BytesToAddress(data[12:32])
// PHASE 5 FIX: Validate recipient address in multi-hop swaps
rawRecipient := common.BytesToAddress(data[12:32])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawRecipient.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
return nil, fmt.Errorf("invalid recipient address in exactInput: %s (corruption score: %d)", rawRecipient.Hex(), validation.CorruptionScore)
}
}
recipient := rawRecipient
deadline := new(big.Int).SetBytes(data[32:64])
amountIn := new(big.Int).SetBytes(data[64:96])
amountOutMinimum := new(big.Int).SetBytes(data[96:128])
// Extract first and last tokens from path (simplified)
pathOffset := new(big.Int).SetBytes(data[128:160]).Uint64()
if len(data) < int(pathOffset)+64 {
if len(data) < safeConvertUint64ToInt(pathOffset)+64 {
return nil, fmt.Errorf("invalid path data")
}
tokenIn := common.BytesToAddress(data[pathOffset+12 : pathOffset+32])
// PHASE 5 FIX: Validate tokenIn in multi-hop path
rawTokenIn := common.BytesToAddress(data[pathOffset+12 : pathOffset+32])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawTokenIn.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
return nil, fmt.Errorf("invalid tokenIn in exactInput path: %s (corruption score: %d)", rawTokenIn.Hex(), validation.CorruptionScore)
}
}
tokenIn := rawTokenIn
// For multi-hop, we'd need to parse the full path, simplified here
tokenOut := common.Address{} // Would extract from end of path
@@ -520,6 +615,57 @@ func (p *sophisticatedABIDecoder) decodeExactOutput(data []byte) (*SwapEvent, er
return p.decodeExactInput(data) // Similar structure, different semantics
}
// decodeUniswapV3Multicall decodes multicall payloads by inspecting embedded calls
func (p *sophisticatedABIDecoder) decodeUniswapV3Multicall(data []byte) (interface{}, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty payload for Uniswap V3 multicall")
}
calls, err := calldata.DecodeMulticallCalls(data)
if err != nil {
return nil, fmt.Errorf("failed to decode multicall calls: %w", err)
}
for _, call := range calls {
if len(call) < 4 {
continue
}
nested, err := p.decodeUniswapV3Swap(call[:4], call[4:])
if err != nil {
continue
}
if swap, ok := nested.(*SwapEvent); ok {
return swap, nil
}
}
// Fallback: derive tokens directly if no nested call decoded successfully
tokens, err := calldata.ExtractTokensFromMulticallWithContext(data, &calldata.MulticallContext{
Protocol: "uniswap_v3",
Stage: "arbitrum.parser.decodeUniswapV3Multicall",
})
if err != nil {
return nil, fmt.Errorf("failed to extract tokens from multicall: %w", err)
}
if len(tokens) == 0 {
return nil, fmt.Errorf("no recognizable swaps found in multicall payload")
}
swap := &SwapEvent{
Timestamp: time.Now(),
Protocol: "uniswap_v3",
TokenIn: tokens[0],
TokenOut: common.Address{},
AmountIn: big.NewInt(0),
AmountOut: big.NewInt(0),
}
if len(tokens) > 1 {
swap.TokenOut = tokens[1]
}
return swap, nil
}
// decodeSushiSwap decodes SushiSwap transactions
func (p *sophisticatedABIDecoder) decodeSushiSwap(methodSig []byte, data []byte) (interface{}, error) {
// SushiSwap uses Uniswap V2 style interface
@@ -585,16 +731,41 @@ func (p *sophisticatedABIDecoder) decodeSwapExactTokensForTokens(data []byte) (*
amountIn := new(big.Int).SetBytes(data[0:32])
amountOutMin := new(big.Int).SetBytes(data[32:64])
pathOffset := new(big.Int).SetBytes(data[64:96]).Uint64()
recipient := common.BytesToAddress(data[108:128])
// PHASE 5 FIX: Validate recipient address in V2 swaps
rawRecipient := common.BytesToAddress(data[108:128])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawRecipient.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
return nil, fmt.Errorf("invalid recipient address in V2 swap: %s (corruption score: %d)", rawRecipient.Hex(), validation.CorruptionScore)
}
}
recipient := rawRecipient
deadline := new(big.Int).SetBytes(data[128:160])
// Extract tokens from path
if len(data) < int(pathOffset)+64 {
if len(data) < safeConvertUint64ToInt(pathOffset)+64 {
return nil, fmt.Errorf("invalid path data")
}
tokenIn := common.BytesToAddress(data[pathOffset+12 : pathOffset+32])
tokenOut := common.BytesToAddress(data[pathOffset+44 : pathOffset+64])
// PHASE 5 FIX: Validate token addresses in V2 swap path
rawTokenIn := common.BytesToAddress(data[pathOffset+12 : pathOffset+32])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawTokenIn.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
return nil, fmt.Errorf("invalid tokenIn in V2 swap path: %s (corruption score: %d)", rawTokenIn.Hex(), validation.CorruptionScore)
}
}
tokenIn := rawTokenIn
rawTokenOut := common.BytesToAddress(data[pathOffset+44 : pathOffset+64])
if p.addressValidator != nil {
validation := p.addressValidator.ValidateAddress(rawTokenOut.Hex())
if !validation.IsValid || validation.CorruptionScore > 30 {
return nil, fmt.Errorf("invalid tokenOut in V2 swap path: %s (corruption score: %d)", rawTokenOut.Hex(), validation.CorruptionScore)
}
}
tokenOut := rawTokenOut
return &SwapEvent{
Timestamp: time.Now(),
@@ -762,7 +933,7 @@ func (p *EnhancedSequencerParser) analyzeTransactionLogs(tx *types.Transaction,
for _, log := range receipt.Logs {
// Check for Swap events from Uniswap V2/V3 style DEXs
if len(log.Topics) > 0 {
swapEvent := p.parseSwapLog(log, receipt)
swapEvent := p.parseSwapLog(log)
if swapEvent != nil {
swapEvent.BlockNumber = receipt.BlockNumber.Uint64()
swapEvent.TxHash = tx.Hash().Hex()
@@ -801,7 +972,7 @@ func (p *EnhancedSequencerParser) identifyDEXProtocol(router common.Address) str
}
// parseSwapLog parses swap events from transaction logs
func (p *EnhancedSequencerParser) parseSwapLog(log *types.Log, receipt *types.Receipt) *SwapEvent {
func (p *EnhancedSequencerParser) parseSwapLog(log *types.Log) *SwapEvent {
// Uniswap V2 Swap event: Swap(address,uint256,uint256,uint256,uint256,address)
uniV2SwapSig := crypto.Keccak256Hash([]byte("Swap(address,uint256,uint256,uint256,uint256,address)"))
// Uniswap V3 Swap event: Swap(address,address,int256,int256,uint160,uint128,int24)
@@ -1021,7 +1192,7 @@ func (p *EnhancedSequencerParser) calculateArbitrage(swap1, swap2 *SwapEvent) *p
// Estimate profit
baseAmount := big.NewInt(1e18) // 1 token
profit := new(big.Int).Mul(baseAmount, big.NewInt(int64(priceDiff*1000)))
profit := new(big.Int).Mul(baseAmount, big.NewInt(safeConvertUint64ToInt64(uint64(priceDiff*1000))))
return &pkgtypes.ArbitrageOpportunity{
Path: []string{swap1.TokenIn.Hex(), swap1.TokenOut.Hex()},
@@ -1063,13 +1234,14 @@ func (p *EnhancedSequencerParser) fetchBlockSafely(ctx context.Context, blockNum
}
// First try to get block header only
header, err := ethClient.HeaderByNumber(ctx, big.NewInt(int64(blockNumber)))
blockNumBigInt := new(big.Int).SetUint64(blockNumber)
header, err := ethClient.HeaderByNumber(ctx, blockNumBigInt)
if err != nil {
return nil, fmt.Errorf("failed to fetch block header: %w", err)
}
// Try to get full block with transactions
block, err := ethClient.BlockByNumber(ctx, big.NewInt(int64(blockNumber)))
block, err := ethClient.BlockByNumber(ctx, blockNumBigInt)
if err != nil {
// If full block fetch fails, we'll use the RPC client to get block data differently
return p.fetchBlockViaRPC(ctx, blockNumber, header)
@@ -1142,7 +1314,7 @@ func (p *EnhancedSequencerParser) processTransactionReceipts(ctx context.Context
// analyzeReceiptLogs analyzes transaction logs for DEX events
func (p *EnhancedSequencerParser) analyzeReceiptLogs(receipt *types.Receipt, blockNumber uint64) {
for _, log := range receipt.Logs {
if swapEvent := p.parseSwapLog(log, receipt); swapEvent != nil {
if swapEvent := p.parseSwapLog(log); swapEvent != nil {
swapEvent.BlockNumber = blockNumber
swapEvent.TxHash = receipt.TxHash.Hex()
swapEvent.GasUsed = receipt.GasUsed

View File

@@ -0,0 +1,69 @@
package parser
import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/fraktal/mev-beta/pkg/calldata"
"github.com/stretchr/testify/require"
)
type multicallFixture struct {
TxHash string `json:"tx_hash"`
Protocol string `json:"protocol"`
CallData string `json:"call_data"`
}
func TestDecodeUniswapV3MulticallFixture(t *testing.T) {
fixturePath := filepath.Join("..", "..", "..", "test", "fixtures", "multicall_samples", "uniswap_v3_usdc_weth.json")
data, err := os.ReadFile(fixturePath)
require.NoError(t, err, "failed to read fixture %s", fixturePath)
var fx multicallFixture
require.NoError(t, json.Unmarshal(data, &fx))
require.NotEmpty(t, fx.CallData)
hexData := strings.TrimPrefix(fx.CallData, "0x")
payload, err := hex.DecodeString(hexData)
require.NoError(t, err)
decoder, err := NewABIDecoder()
require.NoError(t, err)
rawSwap, err := decoder.DecodeSwapTransaction(fx.Protocol, payload)
require.NoError(t, err)
swap, ok := rawSwap.(*SwapEvent)
require.True(t, ok, "expected SwapEvent from fixture decode")
require.Equal(t, "0xaf88d065e77c8cc2239327c5edb3a432268e5831", strings.ToLower(swap.TokenIn.Hex()))
require.Equal(t, "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", strings.ToLower(swap.TokenOut.Hex()))
require.NotEmpty(t, fx.TxHash, "fixture should include tx hash reference for external verification")
}
func TestDecodeDiagnosticMulticallFixture(t *testing.T) {
fixturePath := filepath.Join("..", "..", "..", "test", "fixtures", "multicall_samples", "diagnostic_zero_addresses.json")
data, err := os.ReadFile(fixturePath)
require.NoError(t, err, "failed to read fixture %s", fixturePath)
var fx multicallFixture
require.NoError(t, json.Unmarshal(data, &fx))
require.NotEmpty(t, fx.CallData)
hexData := strings.TrimPrefix(fx.CallData, "0x")
payload, err := hex.DecodeString(hexData)
require.NoError(t, err)
ctx := &calldata.MulticallContext{TxHash: fx.TxHash, Protocol: fx.Protocol, Stage: "fixture-test"}
tokens, err := calldata.ExtractTokensFromMulticallWithContext(payload, ctx)
// With our new validation system, this should now return an error for corrupted data
if err != nil {
require.Error(t, err, "diagnostic fixture with zero addresses should return error due to enhanced validation")
require.Contains(t, err.Error(), "no tokens extracted", "error should indicate no valid tokens found")
} else {
require.Len(t, tokens, 0, "if no error, should yield no valid tokens")
}
}

View File

@@ -0,0 +1,96 @@
package parser
import (
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
const uniswapV3RouterABI = `[
{
"name":"multicall",
"type":"function",
"stateMutability":"payable",
"inputs":[
{"name":"deadline","type":"uint256"},
{"name":"data","type":"bytes[]"}
],
"outputs":[]
},
{
"name":"exactInputSingle",
"type":"function",
"stateMutability":"payable",
"inputs":[
{
"name":"params",
"type":"tuple",
"components":[
{"name":"tokenIn","type":"address"},
{"name":"tokenOut","type":"address"},
{"name":"fee","type":"uint24"},
{"name":"recipient","type":"address"},
{"name":"deadline","type":"uint256"},
{"name":"amountIn","type":"uint256"},
{"name":"amountOutMinimum","type":"uint256"},
{"name":"sqrtPriceLimitX96","type":"uint160"}
]
}
],
"outputs":[{"name":"","type":"uint256"}]
}
]`
type exactInputSingleParams struct {
TokenIn common.Address `abi:"tokenIn"`
TokenOut common.Address `abi:"tokenOut"`
Fee *big.Int `abi:"fee"`
Recipient common.Address `abi:"recipient"`
Deadline *big.Int `abi:"deadline"`
AmountIn *big.Int `abi:"amountIn"`
AmountOutMinimum *big.Int `abi:"amountOutMinimum"`
SqrtPriceLimitX96 *big.Int `abi:"sqrtPriceLimitX96"`
}
func TestDecodeUniswapV3Multicall(t *testing.T) {
decoder, err := NewABIDecoder()
require.NoError(t, err)
routerABI, err := abi.JSON(strings.NewReader(uniswapV3RouterABI))
require.NoError(t, err)
tokenIn := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
tokenOut := common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1")
params := exactInputSingleParams{
TokenIn: tokenIn,
TokenOut: tokenOut,
Fee: big.NewInt(500),
Recipient: common.HexToAddress("0x1111111254eeb25477b68fb85ed929f73a960582"),
Deadline: big.NewInt(0),
AmountIn: big.NewInt(1_000_000),
AmountOutMinimum: big.NewInt(950_000),
SqrtPriceLimitX96: big.NewInt(0),
}
innerCall, err := routerABI.Pack("exactInputSingle", params)
require.NoError(t, err)
multicallPayload, err := routerABI.Pack("multicall", big.NewInt(0), [][]byte{innerCall})
require.NoError(t, err)
rawSwap, err := decoder.DecodeSwapTransaction("uniswap_v3", multicallPayload)
require.NoError(t, err)
swap, ok := rawSwap.(*SwapEvent)
require.True(t, ok, "expected SwapEvent from multicall decode")
require.Equal(t, tokenIn, swap.TokenIn)
require.Equal(t, tokenOut, swap.TokenOut)
require.Equal(t, big.NewInt(1_000_000), swap.AmountIn)
require.Equal(t, big.NewInt(950_000), swap.AmountOut)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/logger"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/market"
"github.com/fraktal/mev-beta/pkg/math"
@@ -480,34 +481,14 @@ func (ta *TransactionAnalyzer) estimateUniswapV2PriceImpact(ctx context.Context,
// Access the market discovery to get real pool reserves
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err != nil || poolInfo == nil {
// Fallback estimation based on amount if pool info not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.01 // 1%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.001 // 0.1%
} else {
return 0.0005 // 0.05%
}
return ta.estimateFallbackV2PriceImpact(swapParams.AmountIn)
}
// Check if pool reserves are properly initialized
// Note: PoolData doesn't have Reserve0/Reserve1, it has Liquidity, SqrtPriceX96, Tick
// So for Uniswap V2, we need liquidity or sqrtPriceX96 values to be non-zero
if poolInfo.Liquidity == nil || poolInfo.Liquidity.Sign() <= 0 {
// Fallback estimation based on amount if pool info not available or improperly initialized
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.01 // 1%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.001 // 0.1%
} else {
return 0.0005 // 0.05%
}
return ta.estimateFallbackV2PriceImpact(swapParams.AmountIn)
}
// Convert uint256 values to big.Int for math engine
@@ -522,22 +503,26 @@ func (ta *TransactionAnalyzer) estimateUniswapV2PriceImpact(ctx context.Context,
// Calculate price impact using approximated reserves
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, reserve0Big, reserve1Big)
if err != nil {
// Fallback if calculation fails
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.01 // 1%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.001 // 0.1%
} else {
return 0.0005 // 0.05%
}
return ta.estimateFallbackV2PriceImpact(swapParams.AmountIn)
}
return impact
}
// estimateFallbackV2PriceImpact provides a fallback estimation for Uniswap V2 based on amount
func (ta *TransactionAnalyzer) estimateFallbackV2PriceImpact(amountIn *big.Int) float64 {
amountFloat, _ := new(big.Float).SetInt(amountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.01 // 1%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.005 // 0.5%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.001 // 0.1%
} else {
return 0.0005 // 0.05%
}
}
// estimateUniswapV3PriceImpact estimates price impact for Uniswap V3
func (ta *TransactionAnalyzer) estimateUniswapV3PriceImpact(ctx context.Context, swapParams *SwapParams, mathEngine math.ExchangeMath) float64 {
// Get actual pool data from market discovery
@@ -546,32 +531,12 @@ func (ta *TransactionAnalyzer) estimateUniswapV3PriceImpact(ctx context.Context,
// Access the market discovery to get real pool data
poolInfo, err := ta.marketDiscovery.GetPool(ctx, poolAddr)
if err != nil || poolInfo == nil || poolInfo.SqrtPriceX96 == nil || poolInfo.Liquidity == nil {
// Fallback estimation based on amount if pool info not available
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.005 // 0.5%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.002 // 0.2%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.0005 // 0.05%
} else {
return 0.0002 // 0.02%
}
return ta.estimateFallbackPriceImpact(swapParams.AmountIn)
}
// Check if pool data is properly initialized
if poolInfo.SqrtPriceX96 == nil || poolInfo.Liquidity == nil {
// Fallback estimation based on amount if pool info not available or improperly initialized
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.005 // 0.5%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.002 // 0.2%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.0005 // 0.05%
} else {
return 0.0002 // 0.02%
}
return ta.estimateFallbackPriceImpact(swapParams.AmountIn)
}
// Convert uint256 values to big.Int for math engine
@@ -581,22 +546,26 @@ func (ta *TransactionAnalyzer) estimateUniswapV3PriceImpact(ctx context.Context,
// Calculate price impact using V3-specific math with converted values
impact, err := mathEngine.CalculatePriceImpact(swapParams.AmountIn, sqrtPriceX96Big, liquidityBig)
if err != nil {
// Fallback if calculation fails
amountFloat, _ := new(big.Float).SetInt(swapParams.AmountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.005 // 0.5%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.002 // 0.2%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.0005 // 0.05%
} else {
return 0.0002 // 0.02%
}
return ta.estimateFallbackPriceImpact(swapParams.AmountIn)
}
return impact
}
// estimateFallbackPriceImpact provides a fallback estimation based on amount
func (ta *TransactionAnalyzer) estimateFallbackPriceImpact(amountIn *big.Int) float64 {
amountFloat, _ := new(big.Float).SetInt(amountIn).Float64()
if amountFloat > 10e18 { // > 10 ETH
return 0.005 // 0.5%
} else if amountFloat > 1e18 { // > 1 ETH
return 0.002 // 0.2%
} else if amountFloat > 0.1e18 { // > 0.1 ETH
return 0.0005 // 0.05%
} else {
return 0.0002 // 0.02%
}
}
// estimateCurvePriceImpact estimates price impact for Curve Finance
func (ta *TransactionAnalyzer) estimateCurvePriceImpact(ctx context.Context, swapParams *SwapParams, mathEngine math.ExchangeMath) float64 {
// Get actual pool data from market discovery
@@ -858,6 +827,10 @@ func (ta *TransactionAnalyzer) findArbitrageOpportunity(ctx context.Context, swa
arbOp.Profit = profit
arbOp.NetProfit = netProfit
arbOp.GasEstimate = gasCost
arbOp.EstimatedProfit = profit
arbOp.RequiredAmount = amountIn
arbOp.DetectedAt = time.Now()
arbOp.ExpiresAt = time.Now().Add(5 * time.Minute)
// Handle empty token addresses to prevent slice bounds panic
tokenInDisplay := "unknown"
@@ -918,8 +891,6 @@ func (ta *TransactionAnalyzer) findArbitrageOpportunity(ctx context.Context, swa
}()
return arbOp
return arbOp
}
func (ta *TransactionAnalyzer) findSandwichOpportunity(ctx context.Context, swapData *SwapData, tx *RawL2Transaction) *SandwichOpportunity {

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/pkg/types"
)