284 lines
9.7 KiB
Go
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()
|
|
}
|
|
}
|