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 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 } 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() 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 } tokenIn, tokenOut, fee := parseUniswapV3Path(pathBytes) 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.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 } tokenOut, tokenIn, fee := parseUniswapV3Path(pathBytes) 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.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.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{}) { return } if sc.Factory == (common.Address{}) || sc.TokenIn == (common.Address{}) || sc.TokenOut == (common.Address{}) { return } fee := int64(sc.Fee) if fee == 0 { fee = 3000 } sc.PoolAddress = uniswap.CalculatePoolAddress(sc.Factory, sc.TokenIn, sc.TokenOut, fee) } func (sc *SwapCall) ensureV2PoolAddress() { if sc.PoolAddress != (common.Address{}) { return } if sc.Factory == (common.Address{}) || sc.TokenIn == (common.Address{}) || sc.TokenOut == (common.Address{}) { return } token0 := sc.TokenIn token1 := sc.TokenOut 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:]) sc.PoolAddress = addr } 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 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) }