// 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/contracts" "github.com/fraktal/mev-beta/bindings/flashswap" "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 *contracts.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 := contracts.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 := contracts.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 := contracts.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) contracts.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 := contracts.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) contracts.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 contracts.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 contracts.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 := contracts.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() } }