Files
mev-beta/pkg/contracts/executor.go
Krypto Kajun 850223a953 fix(multicall): resolve critical multicall parsing corruption issues
- Added comprehensive bounds checking to prevent buffer overruns in multicall parsing
- Implemented graduated validation system (Strict/Moderate/Permissive) to reduce false positives
- Added LRU caching system for address validation with 10-minute TTL
- Enhanced ABI decoder with missing Universal Router and Arbitrum-specific DEX signatures
- Fixed duplicate function declarations and import conflicts across multiple files
- Added error recovery mechanisms with multiple fallback strategies
- Updated tests to handle new validation behavior for suspicious addresses
- Fixed parser test expectations for improved validation system
- Applied gofmt formatting fixes to ensure code style compliance
- Fixed mutex copying issues in monitoring package by introducing MetricsSnapshot
- Resolved critical security vulnerabilities in heuristic address extraction
- Progress: Updated TODO audit from 10% to 35% complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 00:12:55 -05:00

441 lines
15 KiB
Go

// Package contracts provides integration with MEV smart contracts for arbitrage execution
package contracts
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
etypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/bindings/arbitrage"
"github.com/fraktal/mev-beta/bindings/flashswap"
"github.com/fraktal/mev-beta/bindings/interfaces"
"github.com/fraktal/mev-beta/internal/config"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/security"
stypes "github.com/fraktal/mev-beta/pkg/types"
)
// ContractExecutor handles execution of arbitrage opportunities through smart contracts
type ContractExecutor struct {
config *config.BotConfig
logger *logger.Logger
client *ethclient.Client
keyManager *security.KeyManager
arbitrage *arbitrage.ArbitrageExecutor
flashSwapper *flashswap.BaseFlashSwapper
privateKey string
accountAddress common.Address
chainID *big.Int
gasPrice *big.Int
pendingNonce uint64
lastNonceUpdate time.Time
}
// NewContractExecutor creates a new contract executor
func NewContractExecutor(
cfg *config.Config,
logger *logger.Logger,
keyManager *security.KeyManager,
) (*ContractExecutor, error) {
// Connect to Ethereum client
client, err := ethclient.Dial(cfg.Arbitrum.RPCEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
}
// Parse contract addresses from config
arbitrageAddr := common.HexToAddress(cfg.Contracts.ArbitrageExecutor)
flashSwapperAddr := common.HexToAddress(cfg.Contracts.FlashSwapper)
// Create contract instances
arbitrageContract, err := arbitrage.NewArbitrageExecutor(arbitrageAddr, client)
if err != nil {
return nil, fmt.Errorf("failed to instantiate arbitrage contract: %w", err)
}
flashSwapperContract, err := flashswap.NewBaseFlashSwapper(flashSwapperAddr, client)
if err != nil {
return nil, fmt.Errorf("failed to instantiate flash swapper contract: %w", err)
}
// Get chain ID
chainID, err := client.ChainID(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
executor := &ContractExecutor{
config: &cfg.Bot,
logger: logger,
client: client,
keyManager: keyManager,
arbitrage: arbitrageContract,
flashSwapper: flashSwapperContract,
privateKey: "", // Will be retrieved from keyManager when needed
accountAddress: common.Address{}, // Will be retrieved from keyManager when needed
chainID: chainID,
gasPrice: big.NewInt(0),
pendingNonce: 0,
}
// Initialize gas price
if err := executor.updateGasPrice(); err != nil {
logger.Warn(fmt.Sprintf("Failed to initialize gas price: %v", err))
}
logger.Info("Contract executor initialized successfully")
return executor, nil
}
// ExecuteArbitrage executes a standard arbitrage opportunity
func (ce *ContractExecutor) ExecuteArbitrage(ctx context.Context, opportunity stypes.ArbitrageOpportunity) (*etypes.Transaction, error) {
ce.logger.Info(fmt.Sprintf("Executing arbitrage opportunity: %+v", opportunity))
// Convert opportunity to contract parameters
params := ce.convertToArbitrageParams(opportunity)
// Prepare transaction options
opts, err := ce.prepareTransactionOpts(ctx)
if err != nil {
return nil, fmt.Errorf("failed to prepare transaction options: %w", err)
}
// Execute arbitrage through contract - convert interface types using correct field names
arbitrageParams := arbitrage.IArbitrageArbitrageParams{
Tokens: params.Tokens,
Pools: params.Pools,
Amounts: params.Amounts,
SwapData: params.SwapData,
MinProfit: params.MinProfit,
}
tx, err := ce.arbitrage.ExecuteArbitrage(opts, arbitrageParams)
if err != nil {
return nil, fmt.Errorf("failed to execute arbitrage: %w", err)
}
ce.logger.Info(fmt.Sprintf("Arbitrage transaction submitted: %s", tx.Hash().Hex()))
return tx, nil
}
// ExecuteTriangularArbitrage executes a triangular arbitrage opportunity
func (ce *ContractExecutor) ExecuteTriangularArbitrage(ctx context.Context, opportunity stypes.ArbitrageOpportunity) (*etypes.Transaction, error) {
ce.logger.Info(fmt.Sprintf("Executing triangular arbitrage opportunity: %+v", opportunity))
// Convert opportunity to contract parameters
params := ce.convertToTriangularArbitrageParams(opportunity)
// Prepare transaction options
opts, err := ce.prepareTransactionOpts(ctx)
if err != nil {
return nil, fmt.Errorf("failed to prepare transaction options: %w", err)
}
// Execute triangular arbitrage through contract - convert interface types
triangularParams := arbitrage.IArbitrageTriangularArbitrageParams{
TokenA: params.TokenA,
TokenB: params.TokenB,
TokenC: params.TokenC,
PoolAB: params.PoolAB,
PoolBC: params.PoolBC,
PoolCA: params.PoolCA,
AmountIn: params.AmountIn,
MinProfit: params.MinProfit,
SwapDataAB: params.SwapDataAB,
SwapDataBC: params.SwapDataBC,
SwapDataCA: params.SwapDataCA,
}
tx, err := ce.arbitrage.ExecuteTriangularArbitrage(opts, triangularParams)
if err != nil {
return nil, fmt.Errorf("failed to execute triangular arbitrage: %w", err)
}
ce.logger.Info(fmt.Sprintf("Triangular arbitrage transaction submitted: %s", tx.Hash().Hex()))
return tx, nil
}
// convertToArbitrageParams converts a scanner opportunity to contract parameters
func (ce *ContractExecutor) convertToArbitrageParams(opportunity stypes.ArbitrageOpportunity) interfaces.IArbitrageArbitrageParams {
// Convert token addresses
tokens := make([]common.Address, len(opportunity.Path))
for i, token := range opportunity.Path {
tokens[i] = common.HexToAddress(token)
}
// Convert pool addresses
pools := make([]common.Address, len(opportunity.Pools))
for i, pool := range opportunity.Pools {
pools[i] = common.HexToAddress(pool)
}
// Convert amounts (simplified for now)
amounts := make([]*big.Int, len(pools))
for i := range amounts {
// Use a default amount for now - in practice this should be calculated based on optimal trade size
amounts[i] = big.NewInt(1000000000000000000) // 1 ETH equivalent
}
// Convert swap data (empty for now - in practice this would contain encoded swap parameters)
swapData := make([][]byte, len(pools))
for i := range swapData {
swapData[i] = []byte{}
}
// Create parameters struct
params := interfaces.IArbitrageArbitrageParams{
Tokens: tokens,
Pools: pools,
Amounts: amounts,
SwapData: swapData,
MinProfit: opportunity.Profit, // Use estimated profit as minimum required profit
}
return params
}
// convertToTriangularArbitrageParams converts a scanner opportunity to triangular arbitrage parameters
func (ce *ContractExecutor) convertToTriangularArbitrageParams(opportunity stypes.ArbitrageOpportunity) interfaces.IArbitrageTriangularArbitrageParams {
// For triangular arbitrage, we expect exactly 3 tokens forming a triangle
if len(opportunity.Path) < 3 {
ce.logger.Error("Invalid triangular arbitrage path - insufficient tokens")
return interfaces.IArbitrageTriangularArbitrageParams{}
}
// Extract the three tokens
tokenA := common.HexToAddress(opportunity.Path[0])
tokenB := common.HexToAddress(opportunity.Path[1])
tokenC := common.HexToAddress(opportunity.Path[2])
// Extract pools (should be 3 for triangular arbitrage)
if len(opportunity.Pools) < 3 {
ce.logger.Error("Invalid triangular arbitrage pools - insufficient pools")
return interfaces.IArbitrageTriangularArbitrageParams{}
}
poolAB := common.HexToAddress(opportunity.Pools[0])
poolBC := common.HexToAddress(opportunity.Pools[1])
poolCA := common.HexToAddress(opportunity.Pools[2])
// Create parameters struct
// Calculate optimal input amount based on opportunity size
amountIn := opportunity.AmountIn
if amountIn == nil || amountIn.Sign() == 0 {
// Use 10% of estimated profit as input amount for triangular arbitrage
amountIn = new(big.Int).Div(opportunity.Profit, big.NewInt(10))
if amountIn.Cmp(big.NewInt(1000000000000000)) < 0 { // Minimum 0.001 ETH
amountIn = big.NewInt(1000000000000000)
}
}
// Generate swap data for each leg of the triangular arbitrage
swapDataAB, err := ce.generateSwapData(tokenA, tokenB, poolAB)
if err != nil {
ce.logger.Warn(fmt.Sprintf("Failed to generate swap data AB: %v", err))
swapDataAB = []byte{} // Fallback to empty data
}
swapDataBC, err := ce.generateSwapData(tokenB, tokenC, poolBC)
if err != nil {
ce.logger.Warn(fmt.Sprintf("Failed to generate swap data BC: %v", err))
swapDataBC = []byte{} // Fallback to empty data
}
swapDataCA, err := ce.generateSwapData(tokenC, tokenA, poolCA)
if err != nil {
ce.logger.Warn(fmt.Sprintf("Failed to generate swap data CA: %v", err))
swapDataCA = []byte{} // Fallback to empty data
}
params := interfaces.IArbitrageTriangularArbitrageParams{
TokenA: tokenA,
TokenB: tokenB,
TokenC: tokenC,
PoolAB: poolAB,
PoolBC: poolBC,
PoolCA: poolCA,
AmountIn: amountIn,
MinProfit: opportunity.Profit,
SwapDataAB: swapDataAB,
SwapDataBC: swapDataBC,
SwapDataCA: swapDataCA,
}
return params
}
// generateSwapData generates the appropriate swap data based on the pool type
func (ce *ContractExecutor) generateSwapData(tokenIn, tokenOut, pool common.Address) ([]byte, error) {
// Check if this is a Uniswap V3 pool by trying to call the fee function
if fee, err := ce.getUniswapV3Fee(pool); err == nil {
// This is a Uniswap V3 pool - generate V3 swap data
return ce.generateUniswapV3SwapData(tokenIn, tokenOut, fee)
}
// Check if this is a Uniswap V2 pool by trying to call getReserves
if err := ce.checkUniswapV2Pool(pool); err == nil {
// This is a Uniswap V2 pool - generate V2 swap data
return ce.generateUniswapV2SwapData(tokenIn, tokenOut)
}
// Unknown pool type - return empty data
return []byte{}, nil
}
// generateUniswapV3SwapData generates swap data for Uniswap V3 pools
func (ce *ContractExecutor) generateUniswapV3SwapData(tokenIn, tokenOut common.Address, fee uint32) ([]byte, error) {
// Encode the recipient and deadline for the swap
// This is a simplified implementation - production would include more parameters
_ = struct {
TokenIn common.Address
TokenOut common.Address
Fee uint32
Recipient common.Address
Deadline *big.Int
AmountOutMinimum *big.Int
SqrtPriceLimitX96 *big.Int
}{
TokenIn: tokenIn,
TokenOut: tokenOut,
Fee: fee,
Recipient: common.Address{}, // Will be set by contract
Deadline: big.NewInt(time.Now().Add(10 * time.Minute).Unix()),
AmountOutMinimum: big.NewInt(1), // Accept any amount for now
SqrtPriceLimitX96: big.NewInt(0), // No price limit
}
// In production, this would use proper ABI encoding
// For now, return a simple encoding
return []byte(fmt.Sprintf("v3:%s:%s:%d", tokenIn.Hex(), tokenOut.Hex(), fee)), nil
}
// generateUniswapV2SwapData generates swap data for Uniswap V2 pools
func (ce *ContractExecutor) generateUniswapV2SwapData(tokenIn, tokenOut common.Address) ([]byte, error) {
// V2 swaps are simpler - just need token addresses and path
_ = struct {
TokenIn common.Address
TokenOut common.Address
To common.Address
Deadline *big.Int
}{
TokenIn: tokenIn,
TokenOut: tokenOut,
To: common.Address{}, // Will be set by contract
Deadline: big.NewInt(time.Now().Add(10 * time.Minute).Unix()),
}
// Simple encoding for V2 swaps
return []byte(fmt.Sprintf("v2:%s:%s", tokenIn.Hex(), tokenOut.Hex())), nil
}
// getUniswapV3Fee tries to get the fee from a Uniswap V3 pool
func (ce *ContractExecutor) getUniswapV3Fee(pool common.Address) (uint32, error) {
// In production, this would call the fee() function on the pool contract
// For now, return a default fee
return 3000, nil // 0.3% fee
}
// checkUniswapV2Pool checks if an address is a Uniswap V2 pool
func (ce *ContractExecutor) checkUniswapV2Pool(pool common.Address) error {
// In production, this would call getReserves() to verify it's a V2 pool
// For now, just return success
return nil
}
// prepareTransactionOpts prepares transaction options with proper gas pricing and nonce
func (ce *ContractExecutor) prepareTransactionOpts(ctx context.Context) (*bind.TransactOpts, error) {
// Update gas price if needed
if err := ce.updateGasPrice(); err != nil {
ce.logger.Warn(fmt.Sprintf("Failed to update gas price: %v", err))
}
// Get current nonce
nonce, err := ce.client.PendingNonceAt(ctx, ce.accountAddress)
if err != nil {
return nil, fmt.Errorf("failed to get account nonce: %w", err)
}
// Check nonce safely before creating transaction options
nonceInt64, err := security.SafeUint64ToInt64(nonce)
if err != nil {
ce.logger.Error("Nonce exceeds int64 maximum", "nonce", nonce, "error", err)
return nil, fmt.Errorf("nonce value exceeds maximum: %w", err)
}
// Create transaction options
opts := &bind.TransactOpts{
From: ce.accountAddress,
Nonce: big.NewInt(nonceInt64),
Signer: ce.signTransaction, // Custom signer function
Value: big.NewInt(0), // No ETH value for arbitrage transactions
GasPrice: ce.gasPrice,
GasLimit: 0, // Let the node estimate gas limit
Context: ctx,
NoSend: false,
}
return opts, nil
}
// updateGasPrice updates the gas price estimate
func (ce *ContractExecutor) updateGasPrice() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Get suggested gas price from node
gasPrice, err := ce.client.SuggestGasPrice(ctx)
if err != nil {
return fmt.Errorf("failed to suggest gas price: %w", err)
}
// Use the suggested gas price directly (no multiplier from config)
ce.gasPrice = gasPrice
return nil
}
// signTransaction signs a transaction with the configured private key
func (ce *ContractExecutor) signTransaction(address common.Address, tx *etypes.Transaction) (*etypes.Transaction, error) {
// Get the private key from the key manager
privateKey, err := ce.keyManager.GetActivePrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to get private key: %w", err)
}
// Get the chain ID for proper signing
chainID, err := ce.client.NetworkID(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
ce.logger.Debug(fmt.Sprintf("Signing transaction with chain ID %s", chainID.String()))
// Create EIP-155 signer for the current chain
signer := etypes.NewEIP155Signer(chainID)
// Sign the transaction
signedTx, err := etypes.SignTx(tx, signer, privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
ce.logger.Debug(fmt.Sprintf("Transaction signed successfully: %s", signedTx.Hash().Hex()))
return signedTx, nil
}
// GetClient returns the ethereum client for external use
func (ce *ContractExecutor) GetClient() *ethclient.Client {
return ce.client
}
// Close closes the contract executor and releases resources
func (ce *ContractExecutor) Close() {
if ce.client != nil {
ce.client.Close()
}
}