package execution import ( "context" "log/slog" "math/big" "os" "testing" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/your-org/mev-bot/pkg/arbitrage" ) func TestNewFlashloanManager(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewFlashloanManager(nil, logger) assert.NotNil(t, manager) assert.NotNil(t, manager.config) assert.NotNil(t, manager.aaveV3Encoder) assert.NotNil(t, manager.uniswapV3Encoder) assert.NotNil(t, manager.uniswapV2Encoder) } func TestDefaultFlashloanConfig(t *testing.T) { config := DefaultFlashloanConfig() assert.NotNil(t, config) assert.Len(t, config.PreferredProviders, 3) assert.Equal(t, FlashloanProviderAaveV3, config.PreferredProviders[0]) assert.Equal(t, uint16(9), config.AaveV3FeeBPS) assert.Equal(t, uint16(0), config.UniswapV3FeeBPS) assert.Equal(t, uint16(30), config.UniswapV2FeeBPS) } func TestFlashloanManager_BuildFlashloanTransaction_AaveV3(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultFlashloanConfig() config.ExecutorContract = common.HexToAddress("0x0000000000000000000000000000000000000001") manager := NewFlashloanManager(config, logger) opp := &arbitrage.Opportunity{ InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), Path: []arbitrage.SwapStep{ { TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, } swapCalldata := []byte{0x01, 0x02, 0x03, 0x04} tx, err := manager.BuildFlashloanTransaction(context.Background(), opp, swapCalldata) require.NoError(t, err) assert.NotNil(t, tx) assert.Equal(t, FlashloanProviderAaveV3, tx.Provider) assert.NotEmpty(t, tx.To) assert.NotEmpty(t, tx.Data) assert.NotNil(t, tx.Fee) assert.True(t, tx.Fee.Cmp(big.NewInt(0)) > 0) // Fee should be > 0 } func TestFlashloanManager_BuildFlashloanTransaction_UniswapV3(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultFlashloanConfig() config.ExecutorContract = common.HexToAddress("0x0000000000000000000000000000000000000001") config.PreferredProviders = []FlashloanProvider{FlashloanProviderUniswapV3} manager := NewFlashloanManager(config, logger) opp := &arbitrage.Opportunity{ InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), Path: []arbitrage.SwapStep{ { TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, } swapCalldata := []byte{0x01, 0x02, 0x03, 0x04} tx, err := manager.BuildFlashloanTransaction(context.Background(), opp, swapCalldata) require.NoError(t, err) assert.NotNil(t, tx) assert.Equal(t, FlashloanProviderUniswapV3, tx.Provider) assert.NotEmpty(t, tx.To) assert.NotEmpty(t, tx.Data) assert.Equal(t, big.NewInt(0), tx.Fee) // UniswapV3 has no separate fee } func TestFlashloanManager_BuildFlashloanTransaction_UniswapV2(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultFlashloanConfig() config.ExecutorContract = common.HexToAddress("0x0000000000000000000000000000000000000001") config.PreferredProviders = []FlashloanProvider{FlashloanProviderUniswapV2} manager := NewFlashloanManager(config, logger) opp := &arbitrage.Opportunity{ InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), Path: []arbitrage.SwapStep{ { TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, } swapCalldata := []byte{0x01, 0x02, 0x03, 0x04} tx, err := manager.BuildFlashloanTransaction(context.Background(), opp, swapCalldata) require.NoError(t, err) assert.NotNil(t, tx) assert.Equal(t, FlashloanProviderUniswapV2, tx.Provider) assert.NotEmpty(t, tx.To) assert.NotEmpty(t, tx.Data) assert.True(t, tx.Fee.Cmp(big.NewInt(0)) > 0) // Fee should be > 0 } func TestFlashloanManager_selectProvider_NoProviders(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := &FlashloanConfig{ PreferredProviders: []FlashloanProvider{}, } manager := NewFlashloanManager(config, logger) token := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") amount := big.NewInt(1e18) _, err := manager.selectProvider(context.Background(), token, amount) assert.Error(t, err) assert.Contains(t, err.Error(), "no flashloan providers configured") } func TestFlashloanManager_calculateFee(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewFlashloanManager(nil, logger) tests := []struct { name string amount *big.Int feeBPS uint16 expectedFee *big.Int }{ { name: "Aave V3 fee (9 bps)", amount: big.NewInt(1e18), feeBPS: 9, expectedFee: big.NewInt(9e14), // 0.0009 * 1e18 }, { name: "Uniswap V2 fee (30 bps)", amount: big.NewInt(1e18), feeBPS: 30, expectedFee: big.NewInt(3e15), // 0.003 * 1e18 }, { name: "Zero fee", amount: big.NewInt(1e18), feeBPS: 0, expectedFee: big.NewInt(0), }, { name: "Small amount", amount: big.NewInt(1000), feeBPS: 9, expectedFee: big.NewInt(0), // Rounds down to 0 }, { name: "Large amount", amount: big.NewInt(1000e18), feeBPS: 9, expectedFee: big.NewInt(9e20), // 0.0009 * 1000e18 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fee := manager.calculateFee(tt.amount, tt.feeBPS) assert.Equal(t, tt.expectedFee, fee) }) } } func TestFlashloanManager_CalculateTotalCost(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) manager := NewFlashloanManager(nil, logger) tests := []struct { name string amount *big.Int feeBPS uint16 expectedTotal *big.Int }{ { name: "Aave V3 cost", amount: big.NewInt(1e18), feeBPS: 9, expectedTotal: big.NewInt(1.0009e18), }, { name: "Uniswap V2 cost", amount: big.NewInt(1e18), feeBPS: 30, expectedTotal: big.NewInt(1.003e18), }, { name: "Zero fee cost", amount: big.NewInt(1e18), feeBPS: 0, expectedTotal: big.NewInt(1e18), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { total := manager.CalculateTotalCost(tt.amount, tt.feeBPS) assert.Equal(t, tt.expectedTotal, total) }) } } func TestAaveV3FlashloanEncoder_EncodeFlashloan(t *testing.T) { encoder := NewAaveV3FlashloanEncoder() assets := []common.Address{ common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), } amounts := []*big.Int{ big.NewInt(1e18), } receiverAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") params := []byte{0x01, 0x02, 0x03, 0x04} to, data, err := encoder.EncodeFlashloan(assets, amounts, receiverAddress, params) require.NoError(t, err) assert.Equal(t, AaveV3PoolAddress, to) assert.NotEmpty(t, data) // Check method ID // flashLoan(address,address[],uint256[],uint256[],address,bytes,uint16) assert.GreaterOrEqual(t, len(data), 4) } func TestAaveV3FlashloanEncoder_EncodeFlashloan_MultipleAssets(t *testing.T) { encoder := NewAaveV3FlashloanEncoder() assets := []common.Address{ common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), } amounts := []*big.Int{ big.NewInt(1e18), big.NewInt(1500e6), } receiverAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") params := []byte{0x01, 0x02, 0x03, 0x04} to, data, err := encoder.EncodeFlashloan(assets, amounts, receiverAddress, params) require.NoError(t, err) assert.Equal(t, AaveV3PoolAddress, to) assert.NotEmpty(t, data) } func TestAaveV3FlashloanEncoder_EncodeFlashloan_EmptyParams(t *testing.T) { encoder := NewAaveV3FlashloanEncoder() assets := []common.Address{ common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), } amounts := []*big.Int{ big.NewInt(1e18), } receiverAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") params := []byte{} to, data, err := encoder.EncodeFlashloan(assets, amounts, receiverAddress, params) require.NoError(t, err) assert.NotEmpty(t, to) assert.NotEmpty(t, data) } func TestUniswapV3FlashloanEncoder_EncodeFlash(t *testing.T) { encoder := NewUniswapV3FlashloanEncoder() token := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") amount := big.NewInt(1e18) poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") recipient := common.HexToAddress("0x0000000000000000000000000000000000000002") data := []byte{0x01, 0x02, 0x03, 0x04} to, calldata, err := encoder.EncodeFlash(token, amount, poolAddress, recipient, data) require.NoError(t, err) assert.Equal(t, poolAddress, to) assert.NotEmpty(t, calldata) // Check method ID // flash(address,uint256,uint256,bytes) assert.GreaterOrEqual(t, len(calldata), 4) } func TestUniswapV3FlashloanEncoder_EncodeFlash_EmptyData(t *testing.T) { encoder := NewUniswapV3FlashloanEncoder() token := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") amount := big.NewInt(1e18) poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") recipient := common.HexToAddress("0x0000000000000000000000000000000000000002") data := []byte{} to, calldata, err := encoder.EncodeFlash(token, amount, poolAddress, recipient, data) require.NoError(t, err) assert.NotEmpty(t, to) assert.NotEmpty(t, calldata) } func TestUniswapV2FlashloanEncoder_EncodeFlash(t *testing.T) { encoder := NewUniswapV2FlashloanEncoder() token := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") amount := big.NewInt(1e18) poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") recipient := common.HexToAddress("0x0000000000000000000000000000000000000002") data := []byte{0x01, 0x02, 0x03, 0x04} to, calldata, err := encoder.EncodeFlash(token, amount, poolAddress, recipient, data) require.NoError(t, err) assert.Equal(t, poolAddress, to) assert.NotEmpty(t, calldata) // Check method ID // swap(uint256,uint256,address,bytes) assert.GreaterOrEqual(t, len(calldata), 4) } func TestUniswapV2FlashloanEncoder_EncodeFlash_EmptyData(t *testing.T) { encoder := NewUniswapV2FlashloanEncoder() token := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") amount := big.NewInt(1e18) poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") recipient := common.HexToAddress("0x0000000000000000000000000000000000000002") data := []byte{} to, calldata, err := encoder.EncodeFlash(token, amount, poolAddress, recipient, data) require.NoError(t, err) assert.NotEmpty(t, to) assert.NotEmpty(t, calldata) } func TestFlashloanManager_ZeroAmount(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultFlashloanConfig() config.ExecutorContract = common.HexToAddress("0x0000000000000000000000000000000000000001") manager := NewFlashloanManager(config, logger) opp := &arbitrage.Opportunity{ InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(0), Path: []arbitrage.SwapStep{ { TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, } swapCalldata := []byte{0x01, 0x02, 0x03, 0x04} tx, err := manager.BuildFlashloanTransaction(context.Background(), opp, swapCalldata) require.NoError(t, err) assert.NotNil(t, tx) assert.Equal(t, big.NewInt(0), tx.Fee) // Fee should be 0 for 0 amount } func TestFlashloanManager_LargeAmount(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultFlashloanConfig() config.ExecutorContract = common.HexToAddress("0x0000000000000000000000000000000000000001") manager := NewFlashloanManager(config, logger) // 1000 ETH largeAmount := new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)) opp := &arbitrage.Opportunity{ InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: largeAmount, Path: []arbitrage.SwapStep{ { TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, } swapCalldata := []byte{0x01, 0x02, 0x03, 0x04} tx, err := manager.BuildFlashloanTransaction(context.Background(), opp, swapCalldata) require.NoError(t, err) assert.NotNil(t, tx) assert.True(t, tx.Fee.Cmp(big.NewInt(0)) > 0) // Verify fee is reasonable (0.09% of 1000 ETH = 0.9 ETH) expectedFee := new(big.Int).Mul(big.NewInt(9e17), big.NewInt(1)) // 0.9 ETH assert.Equal(t, expectedFee, tx.Fee) } // Benchmark tests func BenchmarkFlashloanManager_BuildFlashloanTransaction(b *testing.B) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) config := DefaultFlashloanConfig() config.ExecutorContract = common.HexToAddress("0x0000000000000000000000000000000000000001") manager := NewFlashloanManager(config, logger) opp := &arbitrage.Opportunity{ InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), InputAmount: big.NewInt(1e18), Path: []arbitrage.SwapStep{ { TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"), }, }, } swapCalldata := []byte{0x01, 0x02, 0x03, 0x04} b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = manager.BuildFlashloanTransaction(context.Background(), opp, swapCalldata) } } func BenchmarkAaveV3FlashloanEncoder_EncodeFlashloan(b *testing.B) { encoder := NewAaveV3FlashloanEncoder() assets := []common.Address{ common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), } amounts := []*big.Int{ big.NewInt(1e18), } receiverAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") params := []byte{0x01, 0x02, 0x03, 0x04} b.ResetTimer() for i := 0; i < b.N; i++ { _, _, _ = encoder.EncodeFlashloan(assets, amounts, receiverAddress, params) } }