feat: create v2-prep branch with comprehensive planning
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>
This commit is contained in:
1083
orig/pkg/calldata/multicall.go
Normal file
1083
orig/pkg/calldata/multicall.go
Normal file
File diff suppressed because it is too large
Load Diff
234
orig/pkg/calldata/multicall_test.go
Normal file
234
orig/pkg/calldata/multicall_test.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package calldata
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testRouterABI = `[
|
||||
{
|
||||
"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 TestExtractTokensFromMulticall(t *testing.T) {
|
||||
routerABI, err := abi.JSON(strings.NewReader(testRouterABI))
|
||||
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(900_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)
|
||||
|
||||
calls, err := decodeMulticallCalls(multicallPayload[4:])
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, calls)
|
||||
t.Logf("selector bytes: %x", calls[0][:4])
|
||||
callTokens, err := extractTokensFromCall(calls[0])
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, callTokens)
|
||||
|
||||
tokens, err := ExtractTokensFromMulticall(multicallPayload[4:])
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 2)
|
||||
require.Equal(t, tokenIn, tokens[0])
|
||||
require.Equal(t, tokenOut, tokens[1])
|
||||
}
|
||||
|
||||
func TestExtractTokensFromMulticallBytesOnly(t *testing.T) {
|
||||
routerABI, err := abi.JSON(strings.NewReader(testRouterABI))
|
||||
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(900_000),
|
||||
SqrtPriceLimitX96: big.NewInt(0),
|
||||
}
|
||||
|
||||
innerCall, err := routerABI.Pack("exactInputSingle", params)
|
||||
require.NoError(t, err)
|
||||
|
||||
bytesArrayType, err := abi.NewType("bytes[]", "", nil)
|
||||
require.NoError(t, err)
|
||||
args := abi.Arguments{{Name: "data", Type: bytesArrayType}}
|
||||
encodedArgs, err := args.Pack([][]byte{innerCall})
|
||||
require.NoError(t, err)
|
||||
|
||||
selector := crypto.Keccak256([]byte("multicall(bytes[])"))[:4]
|
||||
multicallPayload := append(selector, encodedArgs...)
|
||||
|
||||
calls, err := decodeMulticallCalls(multicallPayload[4:])
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, calls)
|
||||
|
||||
tokens, err := ExtractTokensFromMulticall(multicallPayload[4:])
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 2)
|
||||
require.Equal(t, tokenIn, tokens[0])
|
||||
require.Equal(t, tokenOut, tokens[1])
|
||||
}
|
||||
|
||||
func TestExtractTokensFromMulticallFiltersSuspiciousTokens(t *testing.T) {
|
||||
routerABI, err := abi.JSON(strings.NewReader(testRouterABI))
|
||||
require.NoError(t, err)
|
||||
|
||||
tokenIn := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
|
||||
suspicious := common.HexToAddress("0x0000000000000000000000000000000000001234")
|
||||
|
||||
params := exactInputSingleParams{
|
||||
TokenIn: tokenIn,
|
||||
TokenOut: suspicious,
|
||||
Fee: big.NewInt(500),
|
||||
Recipient: common.HexToAddress("0x1111111254eeb25477b68fb85ed929f73a960582"),
|
||||
Deadline: big.NewInt(0),
|
||||
AmountIn: big.NewInt(1_000_000),
|
||||
AmountOutMinimum: big.NewInt(900_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)
|
||||
|
||||
tokens, err := ExtractTokensFromMulticall(multicallPayload[4:])
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, tokenIn, tokens[0])
|
||||
}
|
||||
|
||||
func TestExtractTokensFromMulticallAllSuspicious(t *testing.T) {
|
||||
routerABI, err := abi.JSON(strings.NewReader(testRouterABI))
|
||||
require.NoError(t, err)
|
||||
|
||||
suspiciousIn := common.HexToAddress("0x0000000000000000000000000000000000000005")
|
||||
suspiciousOut := common.HexToAddress("0x0000000000000000000000000000000000001234")
|
||||
|
||||
params := exactInputSingleParams{
|
||||
TokenIn: suspiciousIn,
|
||||
TokenOut: suspiciousOut,
|
||||
Fee: big.NewInt(500),
|
||||
Recipient: common.HexToAddress("0x1111111254eeb25477b68fb85ed929f73a960582"),
|
||||
Deadline: big.NewInt(0),
|
||||
AmountIn: big.NewInt(1_000_000),
|
||||
AmountOutMinimum: big.NewInt(900_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)
|
||||
|
||||
tokens, err := ExtractTokensFromMulticall(multicallPayload[4:])
|
||||
// CRITICAL FIX: Updated test to expect new error behavior when all addresses are suspicious
|
||||
if err != nil {
|
||||
// New behavior: error returned when no valid tokens found
|
||||
require.Contains(t, err.Error(), "no tokens extracted")
|
||||
require.Len(t, tokens, 0)
|
||||
} else {
|
||||
// Fallback: empty result if no error returned
|
||||
require.Len(t, tokens, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTokensFromMulticallHeuristicFallback(t *testing.T) {
|
||||
routerABI, err := abi.JSON(strings.NewReader(testRouterABI))
|
||||
require.NoError(t, err)
|
||||
|
||||
tokenIn := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
|
||||
tokenOut := common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1")
|
||||
|
||||
unknownCall := make([]byte, 4+64)
|
||||
copy(unknownCall[:4], []byte{0xde, 0xad, 0xbe, 0xef})
|
||||
copy(unknownCall[4+12:4+32], tokenIn.Bytes())
|
||||
copy(unknownCall[4+32+12:4+64], tokenOut.Bytes())
|
||||
|
||||
multicallPayload, err := routerABI.Pack("multicall", big.NewInt(0), [][]byte{unknownCall})
|
||||
require.NoError(t, err)
|
||||
|
||||
tokens, err := ExtractTokensFromMulticall(multicallPayload[4:])
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tokens, 2)
|
||||
require.Equal(t, tokenIn, tokens[0])
|
||||
require.Equal(t, tokenOut, tokens[1])
|
||||
}
|
||||
|
||||
func TestIsLikelyValidTokenRecognizesKnownTokens(t *testing.T) {
|
||||
validator := getAddressValidator()
|
||||
addr := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
|
||||
if !isLikelyValidToken(addr, validator) {
|
||||
t.Fatalf("expected known token address to be considered valid")
|
||||
}
|
||||
}
|
||||
728
orig/pkg/calldata/swaps.go
Normal file
728
orig/pkg/calldata/swaps.go
Normal file
@@ -0,0 +1,728 @@
|
||||
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)
|
||||
}
|
||||
175
orig/pkg/calldata/swaps_payload_test.go
Normal file
175
orig/pkg/calldata/swaps_payload_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package calldata
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type payloadFixture struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Function string `json:"function"`
|
||||
FunctionSig string `json:"function_sig"`
|
||||
InputData string `json:"input_data"`
|
||||
}
|
||||
|
||||
func loadPayload(t *testing.T, name string) payloadFixture {
|
||||
t.Helper()
|
||||
fullPath := filepath.Join("testdata", "payloads", name)
|
||||
raw, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read payload fixture %s: %v", name, err)
|
||||
}
|
||||
var pf payloadFixture
|
||||
if err := json.Unmarshal(raw, &pf); err != nil {
|
||||
t.Fatalf("failed to unmarshal payload %s: %v", name, err)
|
||||
}
|
||||
return pf
|
||||
}
|
||||
|
||||
func decodeDirect(raw []byte, ctx *MulticallContext) []*SwapCall {
|
||||
validator := getAddressValidator()
|
||||
return decodeSwapCallRecursive(raw, ctx, validator, 0)
|
||||
}
|
||||
|
||||
func TestCapturedPayloadDecoding(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expectSwaps int
|
||||
expectSelector string
|
||||
tokenIn string
|
||||
tokenOut string
|
||||
verifyProtocol string
|
||||
isMulticall bool
|
||||
}{
|
||||
{
|
||||
name: "uniswap_v3_exact_output_single",
|
||||
fixture: "uniswapv3_exact_output_single.json",
|
||||
expectSwaps: 1,
|
||||
expectSelector: "db3e2198",
|
||||
tokenIn: "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8",
|
||||
tokenOut: "0x912ce59144191c1204e64559fe8253a0e49e6548",
|
||||
verifyProtocol: "UniswapV3",
|
||||
},
|
||||
{
|
||||
name: "uniswap_v3_exact_input_single",
|
||||
fixture: "uniswapv3_exact_input_single.json",
|
||||
expectSwaps: 1,
|
||||
expectSelector: "414bf389",
|
||||
tokenIn: "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
|
||||
tokenOut: "0x440017a1b021006d556d7fc06a54c32e42eb745b",
|
||||
verifyProtocol: "UniswapV3",
|
||||
},
|
||||
{
|
||||
name: "uniswap_v2_swap_exact_tokens",
|
||||
fixture: "uniswapv2_exact_tokens.json",
|
||||
expectSwaps: 1,
|
||||
expectSelector: "38ed1739",
|
||||
tokenIn: "0x03f6921f6e948016631ce796331294d5a863a9ee",
|
||||
tokenOut: "0xdcc9691793633176acf5cfdc1a658cb3b982e2fb",
|
||||
verifyProtocol: "UniswapV2",
|
||||
},
|
||||
{
|
||||
name: "multicall_without_recognised_swaps",
|
||||
fixture: "multicall_uniswap.json",
|
||||
expectSwaps: 0,
|
||||
isMulticall: true,
|
||||
},
|
||||
{
|
||||
name: "uniswap_v3_decrease_liquidity",
|
||||
fixture: "uniswapv3_decrease_liquidity.json",
|
||||
expectSwaps: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
payload := loadPayload(t, tc.fixture)
|
||||
raw, err := hex.DecodeString(strings.TrimPrefix(payload.InputData, "0x"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode input data: %v", err)
|
||||
}
|
||||
if len(raw) < 4 {
|
||||
t.Fatalf("payload too short: %d bytes", len(raw))
|
||||
}
|
||||
|
||||
ctx := &MulticallContext{Protocol: payload.Protocol}
|
||||
var swaps []*SwapCall
|
||||
if tc.isMulticall {
|
||||
swaps, err = DecodeSwapCallsFromMulticall(raw[4:], ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("multicall decode failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
swaps = decodeDirect(raw, ctx)
|
||||
}
|
||||
|
||||
if len(swaps) != tc.expectSwaps {
|
||||
selector := strings.ToLower(hex.EncodeToString(raw[:4]))
|
||||
if !tc.isMulticall {
|
||||
// attempt direct decode based on selector for diagnostics
|
||||
var diag *SwapCall
|
||||
switch selector {
|
||||
case "414bf389":
|
||||
diag = decodeExactInputSingle(raw[4:], ctx)
|
||||
case "db3e2198":
|
||||
diag = decodeExactInput(raw[4:], ctx)
|
||||
case "38ed1739":
|
||||
diag = decodeUniswapV2Swap(selector, raw[4:], ctx)
|
||||
}
|
||||
if diag != nil {
|
||||
t.Fatalf("expected %d swaps, got %d (selector=%s, diag=%s)", tc.expectSwaps, len(swaps), selector, diag.String())
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected %d swaps, got %d (selector=%s)", tc.expectSwaps, len(swaps), selector)
|
||||
}
|
||||
|
||||
if tc.expectSwaps == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
swap := swaps[0]
|
||||
if !selectorsMatch(swap.Selector, tc.expectSelector) {
|
||||
t.Fatalf("expected selector %s, got %s", tc.expectSelector, swap.Selector)
|
||||
}
|
||||
if !strings.EqualFold(swap.TokenIn.Hex(), tc.tokenIn) {
|
||||
t.Fatalf("expected tokenIn %s, got %s", tc.tokenIn, swap.TokenIn.Hex())
|
||||
}
|
||||
if !strings.EqualFold(swap.TokenOut.Hex(), tc.tokenOut) {
|
||||
t.Fatalf("expected tokenOut %s, got %s", tc.tokenOut, swap.TokenOut.Hex())
|
||||
}
|
||||
if tc.verifyProtocol != "" && !strings.EqualFold(swap.Protocol, tc.verifyProtocol) {
|
||||
t.Fatalf("expected protocol %s, got %s", tc.verifyProtocol, swap.Protocol)
|
||||
}
|
||||
|
||||
if selectorEquals(tc.expectSelector, "db3e2198") {
|
||||
if swap.AmountOut == nil || swap.AmountOut.Sign() == 0 {
|
||||
t.Fatalf("expected non-zero amountOut for exactOutputSingle")
|
||||
}
|
||||
}
|
||||
|
||||
if selectorEquals(tc.expectSelector, "414bf389") || selectorEquals(tc.expectSelector, "38ed1739") {
|
||||
if swap.AmountIn == nil || swap.AmountIn.Sign() == 0 {
|
||||
t.Fatalf("expected non-zero amountIn for selector %s", tc.expectSelector)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSelectorHex(sel string) string {
|
||||
s := strings.TrimSpace(strings.ToLower(sel))
|
||||
return strings.TrimPrefix(s, "0x")
|
||||
}
|
||||
|
||||
func selectorEquals(a, b string) bool {
|
||||
return normalizeSelectorHex(a) == normalizeSelectorHex(b)
|
||||
}
|
||||
|
||||
func selectorsMatch(actual, expected string) bool {
|
||||
return selectorEquals(actual, expected)
|
||||
}
|
||||
12
orig/pkg/calldata/testdata/payloads/multicall_uniswap.json
vendored
Normal file
12
orig/pkg/calldata/testdata/payloads/multicall_uniswap.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"block_number": "",
|
||||
"contract_name": "TraderJoeRouter",
|
||||
"from": "0x3855808a7f42dbaebacf07291e0ae0a7ed692ecb",
|
||||
"function": "multicall",
|
||||
"function_sig": "0xac9650d8",
|
||||
"hash": "0xaf6228fcef1fa34dafd4e8d6e359b845e052a1a6597c88c1c2c94045c6140f9f",
|
||||
"input_data": "0xac9650d800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000447d39aaf100000000000000000000000031ef83a530fde1b38ee9a18093a333d8bbbc40d50000000000000000000000000000000000000000000000000002d407c9f880c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000344f59c48eb0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000098d7803aea152ff1009f000000000000000000000000000000000000000000000000000000000000000012f0569e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000031a27787e450000000000000000000000000000000000000000000000000002d407c9f880c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000003855808a7f42dbaebacf07291e0ae0a7ed692ecb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff00000000000000000000000000000000000001000000000000000000000000de967676db7b1ccdba2bd94b01b5b19de4b563e4000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"protocol": "Multicall",
|
||||
"to": "0x87d66368cd08a7ca42252f5ab44b2fb6d1fb8d15",
|
||||
"value": "796079871787200"
|
||||
}
|
||||
12
orig/pkg/calldata/testdata/payloads/uniswapv2_exact_tokens.json
vendored
Normal file
12
orig/pkg/calldata/testdata/payloads/uniswapv2_exact_tokens.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"block_number": "",
|
||||
"contract_name": "UniswapV2Router02",
|
||||
"from": "0xba7a224852693f9a1695334105c2e63159eb2311",
|
||||
"function": "swapExactTokensForTokens",
|
||||
"function_sig": "0x38ed1739",
|
||||
"hash": "0xee2af12f5e40c87e7452f70ef439bcf22663a726cc1bdf9f0def2e0f33f72320",
|
||||
"input_data": "0x38ed17390000000000000000000000000000000000000000000000053220a269ab55c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000ba7a224852693f9a1695334105c2e63159eb23110000000000000000000000000000000000000000000000000000000068ed12b5000000000000000000000000000000000000000000000000000000000000000200000000000000000000000003f6921f6e948016631ce796331294d5a863a9ee000000000000000000000000dcc9691793633176acf5cfdc1a658cb3b982e2fb",
|
||||
"protocol": "UniswapV2",
|
||||
"to": "0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24",
|
||||
"value": "0"
|
||||
}
|
||||
12
orig/pkg/calldata/testdata/payloads/uniswapv3_decrease_liquidity.json
vendored
Normal file
12
orig/pkg/calldata/testdata/payloads/uniswapv3_decrease_liquidity.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"block_number": "",
|
||||
"contract_name": "UniswapV3PositionManager",
|
||||
"from": "0xe5e58921dfa6602792e3f5624e91d291c01dc135",
|
||||
"function": "decreaseLiquidity",
|
||||
"function_sig": "0x0c49ccbe",
|
||||
"hash": "0x5992966aa1cc733367aa0a00e2b3098b6d0d6b88835affbc43e02e1101ac4b8b",
|
||||
"input_data": "0x0c49ccbe00000000000000000000000000000000000000000000000000000000004c1f2c00000000000000000000000000000000000000000000000000000001e20e0177000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068ed105b",
|
||||
"protocol": "UniswapV3",
|
||||
"to": "0xc36442b4a4522e871399cd717abdd847ab11fe88",
|
||||
"value": "0"
|
||||
}
|
||||
12
orig/pkg/calldata/testdata/payloads/uniswapv3_exact_input_single.json
vendored
Normal file
12
orig/pkg/calldata/testdata/payloads/uniswapv3_exact_input_single.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"block_number": "",
|
||||
"contract_name": "UniswapV3Router",
|
||||
"from": "0x196beae17c9577256a4c20d72a3c01cae5d00e9e",
|
||||
"function": "exactInputSingle",
|
||||
"function_sig": "0x414bf389",
|
||||
"hash": "0xd39c510f44355cca3b7ee947b9697ef95df8921c8a90b3639572382092afe90c",
|
||||
"input_data": "0x414bf38900000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000440017a1b021006d556d7fc06a54c32e42eb745b0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000196beae17c9577256a4c20d72a3c01cae5d00e9e0000000000000000000000000000000000000000000000000000000068ed0e0e0000000000000000000000000000000000000000000000000024a689f2b2e3f0000000000000000000000000000000000000000000000011470476b682d78b380000000000000000000000000000000000000000000000000000000000000000",
|
||||
"protocol": "UniswapV3",
|
||||
"to": "0xe592427a0aece92de3edee1f18e0157c05861564",
|
||||
"value": "0"
|
||||
}
|
||||
12
orig/pkg/calldata/testdata/payloads/uniswapv3_exact_output_single.json
vendored
Normal file
12
orig/pkg/calldata/testdata/payloads/uniswapv3_exact_output_single.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"block_number": "",
|
||||
"contract_name": "UniswapV3Router",
|
||||
"from": "0x8cc6ab9ac1d1b7c5f6fc33f767aded9a305744e3",
|
||||
"function": "exactOutputSingle",
|
||||
"function_sig": "0xdb3e2198",
|
||||
"hash": "0x69bad4eca82a4e139aad810777dc72faf5414e338b0a1b648e8472cd4904f93e",
|
||||
"input_data": "0xdb3e2198000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc8000000000000000000000000912ce59144191c1204e64559fe8253a0e49e654800000000000000000000000000000000000000000000000000000000000001f40000000000000000000000008cc6ab9ac1d1b7c5f6fc33f767aded9a305744e30000000000000000000000000000000000000000000000000000000068ed0bbd00000000000000000000000000000000000000000000001b1ae4d6e2ef500000000000000000000000000000000000000000000000000000000000000a482cf40000000000000000000000000000000000000000000000000000000000000000",
|
||||
"protocol": "UniswapV3",
|
||||
"to": "0xe592427a0aece92de3edee1f18e0157c05861564",
|
||||
"value": "0"
|
||||
}
|
||||
Reference in New Issue
Block a user