Files
mev-beta/pkg/contracts/executor.go
2025-09-16 11:05:47 -05:00

284 lines
9.7 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"
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
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,
) (*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,
arbitrage: arbitrageContract,
flashSwapper: flashSwapperContract,
privateKey: cfg.Ethereum.PrivateKey,
accountAddress: common.HexToAddress(cfg.Ethereum.AccountAddress),
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
tx, err := ce.arbitrage.ExecuteArbitrage(opts, params)
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
tx, err := ce.arbitrage.ExecuteTriangularArbitrage(opts, params)
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
params := interfaces.IArbitrageTriangularArbitrageParams{
TokenA: tokenA,
TokenB: tokenB,
TokenC: tokenC,
PoolAB: poolAB,
PoolBC: poolBC,
PoolCA: poolCA,
AmountIn: big.NewInt(1000000000000000000), // 1 ETH equivalent (placeholder)
MinProfit: opportunity.Profit, // Use estimated profit as minimum required profit
SwapDataAB: []byte{}, // Placeholder for actual swap data
SwapDataBC: []byte{}, // Placeholder for actual swap data
SwapDataCA: []byte{}, // Placeholder for actual swap data
}
return params
}
// 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)
}
// Create transaction options
opts := &bind.TransactOpts{
From: ce.accountAddress,
Nonce: big.NewInt(int64(nonce)),
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)
}
// Apply gas price multiplier from config (if set)
if ce.config.Ethereum.GasPriceMultiplier > 1.0 {
multiplier := big.NewFloat(ce.config.Ethereum.GasPriceMultiplier)
gasPriceFloat := new(big.Float).SetInt(gasPrice)
adjustedGasPriceFloat := new(big.Float).Mul(gasPriceFloat, multiplier)
adjustedGasPrice, _ := adjustedGasPriceFloat.Int(nil)
ce.gasPrice = adjustedGasPrice
} else {
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) {
// In a production implementation, you would use the private key to sign the transaction
// For now, we'll return the transaction as-is since we're using the client's built-in signing
ce.logger.Debug("Signing transaction (placeholder)")
return tx, nil
}
// Close closes the contract executor and releases resources
func (ce *ContractExecutor) Close() {
if ce.client != nil {
ce.client.Close()
}
}