package execution import ( "fmt" "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/your-org/mev-bot/pkg/arbitrage" ) // UniswapV3 SwapRouter address on Arbitrum var UniswapV3SwapRouterAddress = common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564") // UniswapV3Encoder encodes transactions for UniswapV3 type UniswapV3Encoder struct { swapRouterAddress common.Address } // NewUniswapV3Encoder creates a new UniswapV3 encoder func NewUniswapV3Encoder() *UniswapV3Encoder { return &UniswapV3Encoder{ swapRouterAddress: UniswapV3SwapRouterAddress, } } // ExactInputSingleParams represents parameters for exactInputSingle type ExactInputSingleParams struct { TokenIn common.Address TokenOut common.Address Fee uint32 Recipient common.Address Deadline *big.Int AmountIn *big.Int AmountOutMinimum *big.Int SqrtPriceLimitX96 *big.Int } // EncodeSwap encodes a single UniswapV3 swap func (e *UniswapV3Encoder) EncodeSwap( tokenIn common.Address, tokenOut common.Address, amountIn *big.Int, minAmountOut *big.Int, poolAddress common.Address, fee uint32, recipient common.Address, deadline time.Time, ) (common.Address, []byte, error) { // exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160)) methodID := crypto.Keccak256([]byte("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))"))[:4] data := make([]byte, 0) data = append(data, methodID...) // Struct offset (always 32 bytes for single struct parameter) data = append(data, padLeft(big.NewInt(32).Bytes(), 32)...) // TokenIn data = append(data, padLeft(tokenIn.Bytes(), 32)...) // TokenOut data = append(data, padLeft(tokenOut.Bytes(), 32)...) // Fee (uint24) data = append(data, padLeft(big.NewInt(int64(fee)).Bytes(), 32)...) // Recipient data = append(data, padLeft(recipient.Bytes(), 32)...) // Deadline deadlineUnix := big.NewInt(deadline.Unix()) data = append(data, padLeft(deadlineUnix.Bytes(), 32)...) // AmountIn data = append(data, padLeft(amountIn.Bytes(), 32)...) // AmountOutMinimum data = append(data, padLeft(minAmountOut.Bytes(), 32)...) // SqrtPriceLimitX96 (0 = no limit) data = append(data, padLeft(big.NewInt(0).Bytes(), 32)...) return e.swapRouterAddress, data, nil } // EncodeMultiHopSwap encodes a multi-hop UniswapV3 swap using exactInput func (e *UniswapV3Encoder) EncodeMultiHopSwap( opp *arbitrage.Opportunity, recipient common.Address, minAmountOut *big.Int, deadline time.Time, ) (common.Address, []byte, error) { if len(opp.Path) < 2 { return common.Address{}, nil, fmt.Errorf("multi-hop requires at least 2 steps") } // Build encoded path for UniswapV3 // Format: tokenIn | fee | tokenOut | fee | tokenOut | ... encodedPath := e.buildEncodedPath(opp) // exactInput((bytes,address,uint256,uint256,uint256)) methodID := crypto.Keccak256([]byte("exactInput((bytes,address,uint256,uint256,uint256))"))[:4] data := make([]byte, 0) data = append(data, methodID...) // Struct offset data = append(data, padLeft(big.NewInt(32).Bytes(), 32)...) // Offset to path bytes (5 * 32 bytes) data = append(data, padLeft(big.NewInt(160).Bytes(), 32)...) // Recipient data = append(data, padLeft(recipient.Bytes(), 32)...) // Deadline deadlineUnix := big.NewInt(deadline.Unix()) data = append(data, padLeft(deadlineUnix.Bytes(), 32)...) // AmountIn data = append(data, padLeft(opp.InputAmount.Bytes(), 32)...) // AmountOutMinimum data = append(data, padLeft(minAmountOut.Bytes(), 32)...) // Path bytes length data = append(data, padLeft(big.NewInt(int64(len(encodedPath))).Bytes(), 32)...) // Path bytes (padded to 32-byte boundary) data = append(data, encodedPath...) // Pad path to 32-byte boundary remainder := len(encodedPath) % 32 if remainder != 0 { padding := make([]byte, 32-remainder) data = append(data, padding...) } return e.swapRouterAddress, data, nil } // buildEncodedPath builds the encoded path for UniswapV3 multi-hop swaps func (e *UniswapV3Encoder) buildEncodedPath(opp *arbitrage.Opportunity) []byte { // Format: token (20 bytes) | fee (3 bytes) | token (20 bytes) | fee (3 bytes) | ... // Total: 20 + (23 * (n-1)) bytes for n tokens path := make([]byte, 0) // First token path = append(path, opp.Path[0].TokenIn.Bytes()...) // For each step, append fee + tokenOut for _, step := range opp.Path { // Fee (3 bytes, uint24) fee := make([]byte, 3) feeInt := big.NewInt(int64(step.Fee)) feeBytes := feeInt.Bytes() copy(fee[3-len(feeBytes):], feeBytes) path = append(path, fee...) // TokenOut (20 bytes) path = append(path, step.TokenOut.Bytes()...) } return path } // EncodeExactOutput encodes an exactOutputSingle swap (output amount specified) func (e *UniswapV3Encoder) EncodeExactOutput( tokenIn common.Address, tokenOut common.Address, amountOut *big.Int, maxAmountIn *big.Int, fee uint32, recipient common.Address, deadline time.Time, ) (common.Address, []byte, error) { // exactOutputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160)) methodID := crypto.Keccak256([]byte("exactOutputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))"))[:4] data := make([]byte, 0) data = append(data, methodID...) // Struct offset data = append(data, padLeft(big.NewInt(32).Bytes(), 32)...) // TokenIn data = append(data, padLeft(tokenIn.Bytes(), 32)...) // TokenOut data = append(data, padLeft(tokenOut.Bytes(), 32)...) // Fee data = append(data, padLeft(big.NewInt(int64(fee)).Bytes(), 32)...) // Recipient data = append(data, padLeft(recipient.Bytes(), 32)...) // Deadline deadlineUnix := big.NewInt(deadline.Unix()) data = append(data, padLeft(deadlineUnix.Bytes(), 32)...) // AmountOut data = append(data, padLeft(amountOut.Bytes(), 32)...) // AmountInMaximum data = append(data, padLeft(maxAmountIn.Bytes(), 32)...) // SqrtPriceLimitX96 (0 = no limit) data = append(data, padLeft(big.NewInt(0).Bytes(), 32)...) return e.swapRouterAddress, data, nil } // EncodeMulticall encodes multiple calls into a single transaction func (e *UniswapV3Encoder) EncodeMulticall( calls [][]byte, deadline time.Time, ) (common.Address, []byte, error) { // multicall(uint256 deadline, bytes[] data) methodID := crypto.Keccak256([]byte("multicall(uint256,bytes[])"))[:4] data := make([]byte, 0) data = append(data, methodID...) // Deadline deadlineUnix := big.NewInt(deadline.Unix()) data = append(data, padLeft(deadlineUnix.Bytes(), 32)...) // Offset to bytes array (64 bytes: 32 for deadline + 32 for offset) data = append(data, padLeft(big.NewInt(64).Bytes(), 32)...) // Array length data = append(data, padLeft(big.NewInt(int64(len(calls))).Bytes(), 32)...) // Calculate offsets for each call currentOffset := int64(32 * len(calls)) // Space for all offsets offsets := make([]int64, len(calls)) for i, call := range calls { offsets[i] = currentOffset // Each call takes: 32 bytes for length + length (padded to 32) currentOffset += 32 + int64((len(call)+31)/32*32) } // Write offsets for _, offset := range offsets { data = append(data, padLeft(big.NewInt(offset).Bytes(), 32)...) } // Write call data for _, call := range calls { // Length data = append(data, padLeft(big.NewInt(int64(len(call))).Bytes(), 32)...) // Data data = append(data, call...) // Padding remainder := len(call) % 32 if remainder != 0 { padding := make([]byte, 32-remainder) data = append(data, padding...) } } return e.swapRouterAddress, data, nil }