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:
@@ -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
|
||||
|
||||
69
pkg/arbitrum/parser/core_multicall_fixture_test.go
Normal file
69
pkg/arbitrum/parser/core_multicall_fixture_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
96
pkg/arbitrum/parser/core_multicall_test.go
Normal file
96
pkg/arbitrum/parser/core_multicall_test.go
Normal 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)
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user