Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
729 lines
20 KiB
Go
729 lines
20 KiB
Go
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)
|
|
}
|