package calldata import ( "encoding/binary" "encoding/hex" "fmt" "math/big" "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/fraktal/mev-beta/internal/validation" "github.com/fraktal/mev-beta/pkg/common/selectors" "github.com/fraktal/mev-beta/pkg/uniswap" ) // safeConvertUint64ToInt is defined in multicall.go to avoid duplication // SwapCall represents a decoded swap-like call discovered inside a multicall payload. type SwapCall struct { Selector string Protocol string TokenIn common.Address TokenOut common.Address Recipient common.Address AmountIn *big.Int AmountOut *big.Int AmountOutMinimum *big.Int Fee uint32 Path []common.Address Factory common.Address PoolAddress common.Address Pools []common.Address Fees []uint32 Deadline *big.Int Raw []byte nested bool heuristicFallback bool } // DecodeSwapCallsFromMulticall walks a multicall payload and returns decoded swap calls. func DecodeSwapCallsFromMulticall(data []byte, ctx *MulticallContext) ([]*SwapCall, error) { calls, err := decodeMulticallCalls(data) if err != nil { return nil, err } validator := getAddressValidator() results := make([]*SwapCall, 0) for _, call := range calls { results = append(results, decodeSwapCallRecursive(call, ctx, validator, 0)...) } if len(results) == 0 && ctx == nil { heuristicTokens := heuristicExtractTokens(calls, validator) if len(heuristicTokens) >= 2 { for i := 0; i+1 < len(heuristicTokens); i += 2 { swap := &SwapCall{ Selector: "heuristic", Protocol: "Unclassified", TokenIn: heuristicTokens[i], TokenOut: heuristicTokens[i+1], heuristicFallback: true, } results = append(results, swap) } } } return results, nil } // DecodeSwapCall attempts to decode a single swap call payload. func DecodeSwapCall(data []byte, ctx *MulticallContext) (*SwapCall, error) { if len(data) < 4 { return nil, fmt.Errorf("payload too short to contain function selector") } selector := normalizedSelector(hex.EncodeToString(data[:4])) swap := decodeDirectSwap(selector, data[4:], ctx) if swap == nil { return nil, fmt.Errorf("unsupported swap selector 0x%s", selector) } swap.Raw = append([]byte(nil), data...) return swap, nil } func normalizedSelector(sel string) string { return strings.TrimPrefix(strings.ToLower(sel), "0x") } func decodeSwapCallRecursive(call []byte, ctx *MulticallContext, validator *validation.AddressValidator, depth int) []*SwapCall { if len(call) < 4 || depth > 6 { return nil } selector := strings.ToLower(hex.EncodeToString(call[:4])) payload := call[4:] switch selector { case normalizedSelector(selectors.UniswapV3MulticallWithDeadline), normalizedSelector(selectors.UniswapV3MulticallWithBlockhash), normalizedSelector(selectors.UniswapV3Multicall): nested, err := DecodeSwapCallsFromMulticall(payload, ctx) if err != nil { recordSuspiciousMulticall(payload, 0, ctx) return nil } markNested(nested) return nested case normalizedSelector(selectors.UniversalRouterExecute), normalizedSelector(selectors.UniversalRouterExecutePayable): return decodeUniversalRouterExecute(payload, ctx, validator, depth+1) } if swap := decodeDirectSwap(selector, payload, ctx); swap != nil { swap.Raw = call return []*SwapCall{swap} } return nil } func decodeDirectSwap(selector string, payload []byte, ctx *MulticallContext) *SwapCall { switch selector { case normalizedSelector(selectors.UniswapV3ExactInputSingle), normalizedSelector(selectors.UniswapV3ExactInputSingleLegacy): return decodeExactInputSingle(payload, ctx) case normalizedSelector(selectors.UniswapV3ExactOutputSingle): return decodeExactOutputSingle(payload, ctx) case normalizedSelector(selectors.UniswapV3ExactInput), normalizedSelector(selectors.UniswapV3ExactInputLegacy): return decodeExactInput(payload, ctx) case normalizedSelector(selectors.UniswapV3ExactOutput), normalizedSelector(selectors.UniswapV3ExactOutputBytes): return decodeExactOutput(payload, ctx) case normalizedSelector(selectors.UniswapV2SwapExactTokensForTokens), normalizedSelector(selectors.UniswapV2SwapTokensForExactTokens), normalizedSelector(selectors.UniswapV2SwapExactETHForTokens), normalizedSelector(selectors.UniswapV2SwapExactTokensForETH), normalizedSelector(selectors.UniswapV2SwapExactTokensForTokensSupportingFee), normalizedSelector(selectors.UniswapV2SwapExactTokensForTokensSupportingFeeLegacy), normalizedSelector(selectors.UniswapV2SwapExactETHForTokensSupportingFee), normalizedSelector(selectors.UniswapV2SwapExactTokensForETHSupportingFee): return decodeUniswapV2Swap(selector, payload, ctx) case normalizedSelector(selectors.NonfungiblePositionManagerMint): return decodePositionManagerMint(payload, ctx) default: return nil } } func decodeUniversalRouterExecute(payload []byte, ctx *MulticallContext, validator *validation.AddressValidator, depth int) []*SwapCall { if len(payload) < 96 { return nil } commandsOffset, ok := readInt(payload[0:32]) if !ok { return nil } inputsOffset, ok := readInt(payload[32:64]) if !ok { return nil } commands, ok := readBytesAt(payload, commandsOffset) if !ok { return nil } inputs, ok := readBytesArrayAt(payload, inputsOffset) if !ok { return nil } var results []*SwapCall for idx, input := range inputs { if len(commands) > idx { // command interpretation can refine protocol selection later if needed. } results = append(results, decodeSwapCallRecursive(input, ctx, validator, depth+1)...) } return results } func decodeExactInputSingle(payload []byte, ctx *MulticallContext) *SwapCall { if len(payload) < 256 { return nil } tokenIn := common.BytesToAddress(payload[12:32]) tokenOut := common.BytesToAddress(payload[44:64]) fee := binary.BigEndian.Uint32(append(make([]byte, 1), payload[93:96]...)) recipient := common.BytesToAddress(payload[108:128]) deadline := new(big.Int).SetBytes(payload[128:160]) amountIn := new(big.Int).SetBytes(payload[160:192]) amountOutMin := new(big.Int).SetBytes(payload[192:224]) swap := &SwapCall{ Selector: selectors.UniswapV3ExactInputSingle, Protocol: defaultProtocol(contextProtocol(ctx), "UniswapV3"), TokenIn: tokenIn, TokenOut: tokenOut, Recipient: recipient, Deadline: deadline, AmountIn: amountIn, AmountOutMinimum: amountOutMin, Fee: uint32(fee), Path: []common.Address{tokenIn, tokenOut}, } swap.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") swap.ensurePoolAddress() swap.Fees = []uint32{uint32(fee)} swap.Fees = []uint32{uint32(fee)} return swap } func decodeExactInput(payload []byte, ctx *MulticallContext) *SwapCall { if len(payload) < 160 { return nil } pathOffset, ok := readInt(payload[0:32]) if !ok { return nil } recipient := common.BytesToAddress(payload[44:64]) deadline := new(big.Int).SetBytes(payload[64:96]) amountIn := new(big.Int).SetBytes(payload[96:128]) amountOutMin := new(big.Int).SetBytes(payload[128:160]) pathBytes, ok := readBytesAt(payload, pathOffset) if !ok || len(pathBytes) < 40 { return nil } tokens, fees := parseUniswapV3FullPath(pathBytes) tokenIn := common.Address{} tokenOut := common.Address{} if len(tokens) >= 1 { tokenIn = tokens[0] } if len(tokens) >= 1 { tokenOut = tokens[len(tokens)-1] } fee := uint32(0) if len(fees) > 0 { fee = fees[0] } swap := &SwapCall{ Selector: selectors.UniswapV3ExactInput, Protocol: defaultProtocol(contextProtocol(ctx), "UniswapV3"), TokenIn: tokenIn, TokenOut: tokenOut, Recipient: recipient, Deadline: deadline, AmountIn: amountIn, AmountOutMinimum: amountOutMin, Fee: fee, } swap.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") swap.Path = tokens swap.Fees = fees swap.ensurePoolAddress() return swap } func decodeExactOutput(payload []byte, ctx *MulticallContext) *SwapCall { if len(payload) < 160 { return nil } pathOffset, ok := readInt(payload[0:32]) if !ok { return nil } recipient := common.BytesToAddress(payload[44:64]) deadline := new(big.Int).SetBytes(payload[64:96]) amountOut := new(big.Int).SetBytes(payload[96:128]) amountInMax := new(big.Int).SetBytes(payload[128:160]) pathBytes, ok := readBytesAt(payload, pathOffset) if !ok || len(pathBytes) < 40 { return nil } tokens, fees := parseUniswapV3FullPath(pathBytes) tokenOut := common.Address{} tokenIn := common.Address{} if len(tokens) >= 1 { tokenOut = tokens[0] } if len(tokens) >= 2 { tokenIn = tokens[len(tokens)-1] } fee := uint32(0) if len(fees) > 0 { fee = fees[len(fees)-1] } swap := &SwapCall{ Selector: selectors.UniswapV3ExactOutput, Protocol: defaultProtocol(contextProtocol(ctx), "UniswapV3"), TokenIn: tokenIn, TokenOut: tokenOut, Recipient: recipient, Deadline: deadline, AmountIn: amountInMax, AmountOut: amountOut, Fee: fee, } swap.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") swap.Path = tokens swap.Fees = fees swap.ensurePoolAddress() return swap } func decodeExactOutputSingle(payload []byte, ctx *MulticallContext) *SwapCall { if len(payload) < 256 { return nil } tokenIn := common.BytesToAddress(payload[12:32]) tokenOut := common.BytesToAddress(payload[44:64]) fee := binary.BigEndian.Uint32(append(make([]byte, 1), payload[93:96]...)) recipient := common.BytesToAddress(payload[108:128]) deadline := new(big.Int).SetBytes(payload[128:160]) amountOut := new(big.Int).SetBytes(payload[160:192]) amountInMax := new(big.Int).SetBytes(payload[192:224]) swap := &SwapCall{ Selector: selectors.UniswapV3ExactOutputSingle, Protocol: defaultProtocol(contextProtocol(ctx), "UniswapV3"), TokenIn: tokenIn, TokenOut: tokenOut, Recipient: recipient, Deadline: deadline, AmountIn: amountInMax, AmountOut: amountOut, Fee: uint32(fee), Path: []common.Address{tokenIn, tokenOut}, } swap.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") swap.Fees = []uint32{uint32(fee)} swap.ensurePoolAddress() return swap } func decodeUniswapV2Swap(selector string, payload []byte, ctx *MulticallContext) *SwapCall { if len(payload) < 160 { return nil } pathOffset, ok := readInt(payload[64:96]) if !ok { return nil } path, ok := readAddressArray(payload, pathOffset) if !ok || len(path) < 2 { return nil } tokenIn := path[0] tokenOut := path[len(path)-1] protocol := defaultProtocol(contextProtocol(ctx), "UniswapV2") amountA := new(big.Int).SetBytes(payload[0:32]) amountB := new(big.Int).SetBytes(payload[32:64]) swap := &SwapCall{ Selector: selector, Protocol: protocol, TokenIn: tokenIn, TokenOut: tokenOut, Path: path, } switch selector { case normalizedSelector(selectors.UniswapV2SwapExactTokensForTokens), normalizedSelector(selectors.UniswapV2SwapExactETHForTokens), normalizedSelector(selectors.UniswapV2SwapExactTokensForTokensSupportingFee), normalizedSelector(selectors.UniswapV2SwapExactTokensForTokensSupportingFeeLegacy), normalizedSelector(selectors.UniswapV2SwapExactETHForTokensSupportingFee), normalizedSelector(selectors.UniswapV2SwapExactTokensForETHSupportingFee): swap.AmountIn = amountA swap.AmountOutMinimum = amountB case normalizedSelector(selectors.UniswapV2SwapTokensForExactTokens), normalizedSelector(selectors.UniswapV2SwapTokensForExactETH), normalizedSelector(selectors.UniswapV2SwapETHForExactTokens): swap.AmountOut = amountA swap.AmountIn = amountB default: swap.AmountIn = amountA swap.AmountOut = amountB } swap.Recipient = common.BytesToAddress(payload[96+12 : 128]) swap.Deadline = new(big.Int).SetBytes(payload[128:160]) if strings.Contains(strings.ToLower(protocol), "sushi") { swap.Factory = common.HexToAddress("0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac") } else { // Default to canonical Arbitrum Uniswap V2 factory (f1d7cc...) set in parser. swap.Factory = common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9") } swap.ensureV2PoolAddress() return swap } func decodePositionManagerMint(payload []byte, ctx *MulticallContext) *SwapCall { if len(payload) < 320 { return nil } paramsOffset, ok := readInt(payload[0:32]) if !ok { return nil } paramsStart := paramsOffset if paramsStart+352 > len(payload) { return nil } params := payload[paramsStart : paramsStart+352] token0 := common.BytesToAddress(params[12:32]) token1 := common.BytesToAddress(params[44:64]) fee := binary.BigEndian.Uint32(append(make([]byte, 1), params[93:96]...)) amount0Desired := new(big.Int).SetBytes(params[160:192]) amount1Desired := new(big.Int).SetBytes(params[192:224]) recipient := common.BytesToAddress(params[276:296]) deadline := new(big.Int).SetBytes(params[296:328]) swap := &SwapCall{ Selector: selectors.NonfungiblePositionManagerMint, Protocol: defaultProtocol(contextProtocol(ctx), "UniswapV3"), TokenIn: token0, TokenOut: token1, Fee: fee, AmountIn: amount0Desired, AmountOut: amount1Desired, Recipient: recipient, Deadline: deadline, } swap.Factory = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") swap.ensurePoolAddress() return swap } func (sc *SwapCall) ensurePoolAddress() { if sc.PoolAddress != (common.Address{}) && len(sc.Pools) > 0 { return } if sc.Factory == (common.Address{}) { return } tokens := sc.Path fees := sc.Fees if len(tokens) < 2 { tokens = []common.Address{sc.TokenIn, sc.TokenOut} } pools := make([]common.Address, 0) for i := 0; i+1 < len(tokens); i++ { fee := int64(0) if len(fees) > i { fee = int64(fees[i]) } else if sc.Fee != 0 { fee = int64(sc.Fee) } else { fee = 3000 } pool := uniswap.CalculatePoolAddress(sc.Factory, tokens[i], tokens[i+1], fee) pools = append(pools, pool) if sc.PoolAddress == (common.Address{}) { sc.PoolAddress = pool } } sc.Pools = pools } func (sc *SwapCall) ensureV2PoolAddress() { if sc.PoolAddress != (common.Address{}) && len(sc.Pools) > 0 { return } if sc.Factory == (common.Address{}) || sc.TokenIn == (common.Address{}) || sc.TokenOut == (common.Address{}) { return } tokens := sc.Path if len(tokens) < 2 { tokens = []common.Address{sc.TokenIn, sc.TokenOut} } pools := make([]common.Address, 0) for i := 0; i+1 < len(tokens); i++ { token0 := tokens[i] token1 := tokens[i+1] 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, sc.Factory.Bytes()...) data = append(data, salt...) data = append(data, initCodeHash.Bytes()...) hash := crypto.Keccak256(data) var addr common.Address copy(addr[:], hash[12:]) pools = append(pools, addr) if sc.PoolAddress == (common.Address{}) { sc.PoolAddress = addr } } sc.Pools = pools } func markNested(calls []*SwapCall) { for _, c := range calls { if c != nil { c.nested = true } } } func contextProtocol(ctx *MulticallContext) string { if ctx == nil { return "" } return ctx.Protocol } func defaultProtocol(primary, fallback string) string { if strings.TrimSpace(primary) != "" { return primary } return fallback } func parseUniswapV3Path(path []byte) (common.Address, common.Address, uint32) { if len(path) < 40 { return common.Address{}, common.Address{}, 0 } tokenIn := common.BytesToAddress(path[:20]) tokenOut := common.BytesToAddress(path[len(path)-20:]) fee := uint32(0) if len(path) >= 23 { feeBytes := path[20:23] fee = uint32(feeBytes[0])<<16 | uint32(feeBytes[1])<<8 | uint32(feeBytes[2]) } return tokenIn, tokenOut, fee } func parseUniswapV3FullPath(path []byte) ([]common.Address, []uint32) { tokens := make([]common.Address, 0) fees := make([]uint32, 0) if len(path) < 20 { return tokens, fees } cursor := 0 token := common.BytesToAddress(path[cursor : cursor+20]) tokens = append(tokens, token) cursor += 20 for cursor+3+20 <= len(path) { feeBytes := path[cursor : cursor+3] fee := uint32(feeBytes[0])<<16 | uint32(feeBytes[1])<<8 | uint32(feeBytes[2]) fees = append(fees, fee) cursor += 3 token = common.BytesToAddress(path[cursor : cursor+20]) tokens = append(tokens, token) cursor += 20 } return tokens, fees } func readInt(word []byte) (int, bool) { if len(word) != 32 { return 0, false } val := new(big.Int).SetBytes(word) if !val.IsUint64() { return 0, false } return safeConvertUint64ToInt(val.Uint64()), true } func readBytesAt(payload []byte, offset int) ([]byte, bool) { cur := offset for depth := 0; depth < 4; depth++ { if cur < 0 || cur+32 > len(payload) { return nil, false } word := new(big.Int).SetBytes(payload[cur : cur+32]) if !word.IsUint64() { return nil, false } val := safeConvertUint64ToInt(word.Uint64()) // Attempt to treat the value as a pointer to the actual data block. if val > 0 { offsets := []int{} candidate := val + cur if candidate >= 0 { offsets = append(offsets, candidate) } offsets = append(offsets, val) followed := false for _, ptr := range offsets { if ptr < 0 || ptr+32 > len(payload) { continue } lengthWord := new(big.Int).SetBytes(payload[ptr : ptr+32]) if !lengthWord.IsUint64() { continue } size := safeConvertUint64ToInt(lengthWord.Uint64()) start := ptr + 32 end := start + size if size >= 0 && end <= len(payload) { return payload[start:end], true } cur = ptr followed = true break } if followed { continue } } // Fallback: treat the value as the length relative to the current offset. size := val start := cur + 32 end := start + size if size >= 0 && end <= len(payload) { return payload[start:end], true } if val == cur { break } cur = val } return nil, false } func readBytesArrayAt(payload []byte, offset int) ([][]byte, bool) { if offset < 0 || offset+32 > len(payload) { return nil, false } length := new(big.Int).SetBytes(payload[offset : offset+32]) if !length.IsUint64() { return nil, false } count := safeConvertUint64ToInt(length.Uint64()) result := make([][]byte, 0, count) for i := 0; i < count; i++ { entryOffsetWord := payload[offset+32+i*32 : offset+32+(i+1)*32] entryOffset, ok := readInt(entryOffsetWord) if !ok { return nil, false } start := offset + entryOffset entry, ok := readBytesAt(payload, start) if !ok { return nil, false } result = append(result, entry) } return result, true } func readAddressArray(payload []byte, offset int) ([]common.Address, bool) { if offset < 0 || offset+32 > len(payload) { return nil, false } length := new(big.Int).SetBytes(payload[offset : offset+32]) if !length.IsUint64() { return nil, false } count := safeConvertUint64ToInt(length.Uint64()) start := offset + 32 end := start + count*32 if count <= 0 || end > len(payload) { return nil, false } addresses := make([]common.Address, 0, count) for i := 0; i < count; i++ { word := payload[start+i*32 : start+(i+1)*32] addresses = append(addresses, common.BytesToAddress(word[12:])) } return addresses, true } func (sc *SwapCall) String() string { return fmt.Sprintf("%s %s->%s selector=%s", sc.Protocol, sc.TokenIn.Hex(), sc.TokenOut.Hex(), sc.Selector) }