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:
@@ -3,11 +3,15 @@ package events
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/holiman/uint256"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/calldata"
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
)
|
||||
|
||||
// EventType represents the type of DEX event
|
||||
@@ -236,6 +240,8 @@ func (ep *EventParser) identifyProtocol(tx *types.Transaction) string {
|
||||
switch sig {
|
||||
case "0xac9650d8": // multicall (Uniswap V3)
|
||||
return "UniswapV3"
|
||||
case "0x1f0464d1": // multicall with blockhash (Uniswap V3)
|
||||
return "UniswapV3"
|
||||
case "0x88316456": // swap (Uniswap V2)
|
||||
return "UniswapV2"
|
||||
case "0x128acb08": // swap (SushiSwap)
|
||||
@@ -296,6 +302,12 @@ func (ep *EventParser) parseUniswapV2Swap(log *types.Log, blockNumber uint64, ti
|
||||
amount1 = new(big.Int).Neg(amount1Out)
|
||||
}
|
||||
|
||||
// DEBUG: Log details about this event creation
|
||||
if log.Address == (common.Address{}) {
|
||||
fmt.Printf("ZERO ADDRESS DEBUG [EVENTS-1]: Creating Event with zero address - BlockNumber: %d, LogIndex: %d, LogTopics: %d, LogData: %d bytes\n",
|
||||
blockNumber, log.Index, len(log.Topics), len(log.Data))
|
||||
}
|
||||
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: "UniswapV2",
|
||||
@@ -507,12 +519,31 @@ func (ep *EventParser) ParseTransaction(tx *types.Transaction, blockNumber uint6
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
case "ac9650d8": // multicall (Uniswap V3)
|
||||
event, err := ep.parseMulticallFromTx(tx, protocol, blockNumber, timestamp)
|
||||
if err != nil {
|
||||
return []*Event{}, fmt.Errorf("failed to parse multicall: %w", err)
|
||||
}
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
case "f305d719": // exactOutputSingle (Uniswap V3)
|
||||
event, err := ep.parseExactOutputSingleFromTx(tx, protocol, blockNumber, timestamp)
|
||||
if err != nil {
|
||||
return []*Event{}, fmt.Errorf("failed to parse exactOutputSingle: %w", err)
|
||||
}
|
||||
if event != nil {
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
default:
|
||||
// For unknown functions, create a basic event
|
||||
// Use router address as fallback since we can't extract tokens
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: protocol,
|
||||
PoolAddress: *tx.To(),
|
||||
PoolAddress: *tx.To(), // Router address as fallback for unknown functions
|
||||
Token0: common.Address{}, // Will be determined from logs
|
||||
Token1: common.Address{}, // Will be determined from logs
|
||||
Amount0: tx.Value(), // Use transaction value as fallback
|
||||
@@ -560,10 +591,13 @@ func (ep *EventParser) parseSwapExactTokensForTokensFromTx(tx *types.Transaction
|
||||
}
|
||||
}
|
||||
|
||||
// Derive actual pool address from token pair
|
||||
poolAddress := ep.derivePoolAddress(token0, token1, protocol)
|
||||
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: protocol,
|
||||
PoolAddress: *tx.To(),
|
||||
PoolAddress: poolAddress,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Amount0: amountIn,
|
||||
@@ -594,10 +628,13 @@ func (ep *EventParser) parseExactInputSingleFromTx(tx *types.Transaction, protoc
|
||||
amountIn := new(big.Int).SetBytes(data[160:192])
|
||||
amountOutMin := new(big.Int).SetBytes(data[192:224])
|
||||
|
||||
// Derive actual pool address from token pair
|
||||
poolAddress := ep.derivePoolAddress(tokenIn, tokenOut, protocol)
|
||||
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: protocol,
|
||||
PoolAddress: *tx.To(), // This is the router, not the pool
|
||||
PoolAddress: poolAddress,
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Amount0: amountIn,
|
||||
@@ -639,18 +676,21 @@ func (ep *EventParser) parseExactInputFromTx(tx *types.Transaction, protocol str
|
||||
// First token (20 bytes)
|
||||
token0 = common.BytesToAddress(data[pathOffset+32 : pathOffset+52])
|
||||
// For multi-hop paths, find the last token
|
||||
// Simple approximation: skip to last token position
|
||||
// Uniswap V3 path format: tokenA(20) + fee(3) + tokenB(20) + fee(3) + tokenC(20)...
|
||||
if pathLength >= 43 { // tokenA(20) + fee(3) + tokenB(20)
|
||||
token1 = common.BytesToAddress(data[pathOffset+55 : pathOffset+75])
|
||||
token1 = common.BytesToAddress(data[pathOffset+32+20+3 : pathOffset+32+20+3+20]) // Skip token0(20) + fee(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derive actual pool address from token pair
|
||||
poolAddress := ep.derivePoolAddress(token0, token1, protocol)
|
||||
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: protocol,
|
||||
PoolAddress: *tx.To(),
|
||||
PoolAddress: poolAddress,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Amount0: amountIn,
|
||||
@@ -712,6 +752,245 @@ func (ep *EventParser) parseSwapExactETHForTokensFromTx(tx *types.Transaction, p
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// parseExactOutputSingleFromTx parses exactOutputSingle from transaction data
|
||||
func (ep *EventParser) parseExactOutputSingleFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) {
|
||||
data := tx.Data()[4:] // Skip function selector
|
||||
|
||||
if len(data) < 256 { // 8 parameters * 32 bytes
|
||||
return nil, fmt.Errorf("insufficient data for exactOutputSingle")
|
||||
}
|
||||
|
||||
// Parse ExactOutputSingleParams struct
|
||||
tokenIn := common.BytesToAddress(data[12:32])
|
||||
tokenOut := common.BytesToAddress(data[44:64])
|
||||
fee := new(big.Int).SetBytes(data[64:96]).Uint64()
|
||||
amountOut := new(big.Int).SetBytes(data[160:192])
|
||||
amountInMaximum := new(big.Int).SetBytes(data[192:224])
|
||||
|
||||
// Derive actual pool address from token pair
|
||||
poolAddress := ep.derivePoolAddress(tokenIn, tokenOut, protocol)
|
||||
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: protocol,
|
||||
PoolAddress: poolAddress,
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Amount0: amountInMaximum, // Maximum input amount
|
||||
Amount1: amountOut, // Exact output amount
|
||||
SqrtPriceX96: uint256.NewInt(0),
|
||||
Liquidity: uint256.NewInt(0),
|
||||
Tick: 0,
|
||||
Timestamp: timestamp,
|
||||
TransactionHash: tx.Hash(),
|
||||
BlockNumber: blockNumber,
|
||||
}
|
||||
|
||||
// Store fee information for later use
|
||||
event.Protocol = fmt.Sprintf("%s_fee_%d", protocol, fee)
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// parseMulticallFromTx parses multicall transactions to extract token addresses and amounts
|
||||
func (ep *EventParser) parseMulticallFromTx(tx *types.Transaction, protocol string, blockNumber uint64, timestamp uint64) (*Event, error) {
|
||||
data := tx.Data()[4:] // Skip function selector
|
||||
|
||||
if len(data) < 64 { // Need at least bytes array offset and length
|
||||
return nil, fmt.Errorf("insufficient data for multicall")
|
||||
}
|
||||
|
||||
// Extract tokens from multicall data using comprehensive scanning
|
||||
tokenCtx := &calldata.MulticallContext{
|
||||
TxHash: tx.Hash().Hex(),
|
||||
Protocol: protocol,
|
||||
Stage: "events.parser.parseMulticallFromTx",
|
||||
BlockNumber: blockNumber,
|
||||
}
|
||||
swap := ep.extractSwapFromMulticallData(data, tokenCtx)
|
||||
|
||||
var (
|
||||
token0 common.Address
|
||||
token1 common.Address
|
||||
amount0 *big.Int
|
||||
amount1 *big.Int
|
||||
poolAddress common.Address
|
||||
)
|
||||
|
||||
if swap != nil {
|
||||
token0 = swap.TokenIn
|
||||
token1 = swap.TokenOut
|
||||
amount0 = swap.AmountIn
|
||||
if swap.AmountOut != nil {
|
||||
amount1 = new(big.Int).Set(swap.AmountOut)
|
||||
} else if swap.AmountOutMinimum != nil {
|
||||
amount1 = new(big.Int).Set(swap.AmountOutMinimum)
|
||||
}
|
||||
if swap.PoolAddress != (common.Address{}) {
|
||||
poolAddress = swap.PoolAddress
|
||||
}
|
||||
if protocol == "" {
|
||||
protocol = swap.Protocol
|
||||
}
|
||||
}
|
||||
|
||||
if poolAddress == (common.Address{}) {
|
||||
if token0 != (common.Address{}) && token1 != (common.Address{}) {
|
||||
poolAddress = ep.derivePoolAddress(token0, token1, protocol)
|
||||
} else if tx.To() != nil {
|
||||
poolAddress = *tx.To()
|
||||
}
|
||||
}
|
||||
|
||||
if amount0 == nil {
|
||||
amount0 = tx.Value()
|
||||
}
|
||||
if amount1 == nil {
|
||||
amount1 = big.NewInt(0)
|
||||
}
|
||||
|
||||
event := &Event{
|
||||
Type: Swap,
|
||||
Protocol: protocol,
|
||||
PoolAddress: poolAddress,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Amount0: amount0,
|
||||
Amount1: amount1,
|
||||
SqrtPriceX96: uint256.NewInt(0),
|
||||
Liquidity: uint256.NewInt(0),
|
||||
Tick: 0,
|
||||
Timestamp: timestamp,
|
||||
TransactionHash: tx.Hash(),
|
||||
BlockNumber: blockNumber,
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// extractSwapFromMulticallData decodes the first viable swap call from multicall payload data.
|
||||
func (ep *EventParser) extractSwapFromMulticallData(data []byte, ctx *calldata.MulticallContext) *calldata.SwapCall {
|
||||
swaps, err := calldata.DecodeSwapCallsFromMulticall(data, ctx)
|
||||
if err != nil || len(swaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, swap := range swaps {
|
||||
if swap == nil {
|
||||
continue
|
||||
}
|
||||
if !ep.isValidTokenAddress(swap.TokenIn) || !ep.isValidTokenAddress(swap.TokenOut) {
|
||||
continue
|
||||
}
|
||||
return swap
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidTokenAddress checks if an address looks like a valid token address
|
||||
func (ep *EventParser) isValidTokenAddress(addr common.Address) bool {
|
||||
// Skip zero address
|
||||
if addr == (common.Address{}) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip known router and factory addresses
|
||||
knownRouters := map[common.Address]bool{
|
||||
ep.UniswapV2Router02: true,
|
||||
ep.UniswapV3Router: true,
|
||||
ep.UniswapV2Factory: true,
|
||||
ep.UniswapV3Factory: true,
|
||||
ep.SushiSwapFactory: true,
|
||||
common.HexToAddress("0xA51afAFe0263b40EdaEf0Df8781eA9aa03E381a3"): true, // Universal Router
|
||||
common.HexToAddress("0x1111111254EEB25477B68fb85Ed929f73A960582"): true, // 1inch Router v5
|
||||
common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"): true, // Uniswap V3 Position Manager
|
||||
}
|
||||
|
||||
if knownRouters[addr] {
|
||||
return false
|
||||
}
|
||||
|
||||
// Basic heuristic: valid token addresses typically have some non-zero bytes
|
||||
// and don't end with many zeros (which are often parameter values)
|
||||
bytes := addr.Bytes()
|
||||
nonZeroCount := 0
|
||||
for _, b := range bytes {
|
||||
if b != 0 {
|
||||
nonZeroCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Require at least 8 non-zero bytes for a valid token address
|
||||
return nonZeroCount >= 8
|
||||
}
|
||||
|
||||
// derivePoolAddress derives the pool address from token pair and protocol
|
||||
func (ep *EventParser) derivePoolAddress(token0, token1 common.Address, protocol string) common.Address {
|
||||
// If either token is zero address, we cannot derive a valid pool address
|
||||
if token0 == (common.Address{}) || token1 == (common.Address{}) {
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
// Check if either token is actually a router address (shouldn't happen but safety check)
|
||||
knownRouters := map[common.Address]bool{
|
||||
ep.UniswapV2Router02: true,
|
||||
ep.UniswapV3Router: true,
|
||||
common.HexToAddress("0xA51afAFe0263b40EdaEf0Df8781eA9aa03E381a3"): true, // Universal Router
|
||||
common.HexToAddress("0x1111111254EEB25477B68fb85Ed929f73A960582"): true, // 1inch Router v5
|
||||
common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"): true, // Uniswap V3 Position Manager
|
||||
}
|
||||
|
||||
if knownRouters[token0] || knownRouters[token1] {
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
protocolLower := strings.ToLower(protocol)
|
||||
if strings.Contains(protocolLower, "uniswapv3") {
|
||||
fee := int64(3000)
|
||||
return uniswap.CalculatePoolAddress(ep.UniswapV3Factory, token0, token1, fee)
|
||||
}
|
||||
|
||||
if strings.Contains(protocolLower, "sushi") {
|
||||
if addr := calculateUniswapV2Pair(ep.SushiSwapFactory, token0, token1); addr != (common.Address{}) {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(protocolLower, "uniswapv2") || strings.Contains(protocolLower, "camelot") {
|
||||
if addr := calculateUniswapV2Pair(ep.UniswapV2Factory, token0, token1); addr != (common.Address{}) {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
func calculateUniswapV2Pair(factory, token0, token1 common.Address) common.Address {
|
||||
if factory == (common.Address{}) || token0 == (common.Address{}) || token1 == (common.Address{}) {
|
||||
return common.Address{}
|
||||
}
|
||||
|
||||
if token0.Big().Cmp(token1.Big()) > 0 {
|
||||
token0, token1 = token1, token0
|
||||
}
|
||||
|
||||
keccakInput := append(token0.Bytes(), token1.Bytes()...)
|
||||
salt := crypto.Keccak256(keccakInput)
|
||||
initCodeHash := common.HexToHash("0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f")
|
||||
|
||||
data := make([]byte, 0, 85)
|
||||
data = append(data, 0xff)
|
||||
data = append(data, factory.Bytes()...)
|
||||
data = append(data, salt...)
|
||||
data = append(data, initCodeHash.Bytes()...)
|
||||
|
||||
hash := crypto.Keccak256(data)
|
||||
var addr common.Address
|
||||
copy(addr[:], hash[12:])
|
||||
return addr
|
||||
}
|
||||
|
||||
// AddKnownPool adds a pool address to the known pools map
|
||||
func (ep *EventParser) AddKnownPool(address common.Address, protocol string) {
|
||||
ep.knownPools[address] = protocol
|
||||
|
||||
Reference in New Issue
Block a user