test(execution): add comprehensive test suite for execution engine
Some checks failed
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled
Some checks failed
V2 CI/CD Pipeline / Pre-Flight Checks (push) Has been cancelled
V2 CI/CD Pipeline / Build & Dependencies (push) Has been cancelled
V2 CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
V2 CI/CD Pipeline / Unit Tests (100% Coverage Required) (push) Has been cancelled
V2 CI/CD Pipeline / Integration Tests (push) Has been cancelled
V2 CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
V2 CI/CD Pipeline / Decimal Precision Validation (push) Has been cancelled
V2 CI/CD Pipeline / Modularity Validation (push) Has been cancelled
V2 CI/CD Pipeline / Final Validation Summary (push) Has been cancelled
Add comprehensive unit tests for all execution engine components: Component Test Coverage: - UniswapV2 encoder: 15 test cases + benchmarks - UniswapV3 encoder: 20 test cases + benchmarks - Curve encoder: 16 test cases + benchmarks - Flashloan manager: 18 test cases + benchmarks - Transaction builder: 15 test cases + benchmarks - Risk manager: 25 test cases + benchmarks - Executor: 20 test cases + benchmarks Test Categories: - Happy path scenarios - Error handling and edge cases - Zero/invalid inputs - Boundary conditions (max amounts, limits) - Concurrent operations (nonce management) - Configuration validation - State management Key Test Features: - Protocol-specific encoding validation - ABI encoding correctness - Gas calculation accuracy - Slippage calculation - Nonce management thread safety - Circuit breaker behavior - Risk assessment rules - Transaction lifecycle Total: 129 test cases + performance benchmarks Target: 100% test coverage for execution engine Related to Phase 4 (Execution Engine) implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
421
pkg/execution/curve_encoder_test.go
Normal file
421
pkg/execution/curve_encoder_test.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCurveEncoder(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
assert.NotNil(t, encoder)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_EncodeSwap(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, poolAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID (first 4 bytes)
|
||||
// exchange(int128,int128,uint256,uint256)
|
||||
assert.Len(t, data, 4+4*32) // methodID + 4 parameters
|
||||
}
|
||||
|
||||
func TestCurveEncoder_EncodeExchangeUnderlying(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
to, data, err := encoder.EncodeExchangeUnderlying(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, poolAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID
|
||||
// exchange_underlying(int128,int128,uint256,uint256)
|
||||
assert.Len(t, data, 4+4*32)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_EncodeDynamicExchange(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
i := big.NewInt(0)
|
||||
j := big.NewInt(1)
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
|
||||
to, data, err := encoder.EncodeDynamicExchange(
|
||||
i,
|
||||
j,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, poolAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID
|
||||
// exchange(uint256,uint256,uint256,uint256)
|
||||
assert.Len(t, data, 4+4*32)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_EncodeDynamicExchange_HighIndices(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
i := big.NewInt(2)
|
||||
j := big.NewInt(3)
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
|
||||
to, data, err := encoder.EncodeDynamicExchange(
|
||||
i,
|
||||
j,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, poolAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_EncodeGetDy(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
i := big.NewInt(0)
|
||||
j := big.NewInt(1)
|
||||
amountIn := big.NewInt(1e18)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
|
||||
to, data, err := encoder.EncodeGetDy(
|
||||
i,
|
||||
j,
|
||||
amountIn,
|
||||
poolAddress,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, poolAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID
|
||||
// get_dy(int128,int128,uint256)
|
||||
assert.Len(t, data, 4+3*32)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_EncodeCoinIndices(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenAddress := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
|
||||
to, data, err := encoder.EncodeCoinIndices(
|
||||
tokenAddress,
|
||||
poolAddress,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, poolAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID
|
||||
// coins(uint256)
|
||||
assert.Len(t, data, 4+32)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_GetCoinIndex(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenAddress common.Address
|
||||
poolCoins []common.Address
|
||||
expectedIndex int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "First coin",
|
||||
tokenAddress: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
poolCoins: []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
},
|
||||
expectedIndex: 0,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Second coin",
|
||||
tokenAddress: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
poolCoins: []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
},
|
||||
expectedIndex: 1,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Third coin",
|
||||
tokenAddress: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
poolCoins: []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
},
|
||||
expectedIndex: 2,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Token not in pool",
|
||||
tokenAddress: common.HexToAddress("0x0000000000000000000000000000000000000099"),
|
||||
poolCoins: []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
},
|
||||
expectedIndex: -1,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Empty pool",
|
||||
tokenAddress: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
poolCoins: []common.Address{},
|
||||
expectedIndex: -1,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
index, err := encoder.GetCoinIndex(tt.tokenAddress, tt.poolCoins)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.expectedIndex, index)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedIndex, index)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurveEncoder_ZeroAddresses(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenIn := common.Address{}
|
||||
tokenOut := common.Address{}
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.Address{}
|
||||
recipient := common.Address{}
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_ZeroAmounts(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(0)
|
||||
minAmountOut := big.NewInt(0)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_LargeAmounts(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
|
||||
// Max uint256
|
||||
amountIn := new(big.Int)
|
||||
amountIn.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10)
|
||||
minAmountOut := new(big.Int)
|
||||
minAmountOut.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10)
|
||||
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_LargeIndices(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
// Test with large indices (for pools with many coins)
|
||||
i := big.NewInt(7)
|
||||
j := big.NewInt(15)
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
|
||||
to, data, err := encoder.EncodeDynamicExchange(
|
||||
i,
|
||||
j,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_NegativeIndices(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
// Negative indices (should be encoded as int128)
|
||||
i := big.NewInt(-1)
|
||||
j := big.NewInt(-2)
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
|
||||
to, data, err := encoder.EncodeDynamicExchange(
|
||||
i,
|
||||
j,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestCurveEncoder_GetCoinIndex_MultipleTokens(t *testing.T) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
// Test with a 4-coin pool (common for Curve)
|
||||
poolCoins := []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDC
|
||||
common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), // USDT
|
||||
common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"), // DAI
|
||||
}
|
||||
|
||||
// Test each token
|
||||
for i, token := range poolCoins {
|
||||
index, err := encoder.GetCoinIndex(token, poolCoins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, i, index)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCurveEncoder_EncodeSwap(b *testing.B) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCurveEncoder_GetCoinIndex(b *testing.B) {
|
||||
encoder := NewCurveEncoder()
|
||||
|
||||
tokenAddress := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
poolCoins := []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
|
||||
common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = encoder.GetCoinIndex(tokenAddress, poolCoins)
|
||||
}
|
||||
}
|
||||
567
pkg/execution/executor_test.go
Normal file
567
pkg/execution/executor_test.go
Normal file
@@ -0,0 +1,567 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/your-org/mev-bot/pkg/arbitrage"
|
||||
mevtypes "github.com/your-org/mev-bot/pkg/types"
|
||||
)
|
||||
|
||||
func TestDefaultExecutorConfig(t *testing.T) {
|
||||
config := DefaultExecutorConfig()
|
||||
|
||||
assert.NotNil(t, config)
|
||||
assert.Equal(t, uint64(1), config.ConfirmationBlocks)
|
||||
assert.Equal(t, 5*time.Minute, config.TimeoutPerTx)
|
||||
assert.Equal(t, 3, config.MaxRetries)
|
||||
assert.Equal(t, uint64(2), config.NonceMargin)
|
||||
assert.Equal(t, "fast", config.GasPriceStrategy)
|
||||
}
|
||||
|
||||
func TestExecutor_getNextNonce(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
currentNonce: 10,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Get first nonce
|
||||
nonce1 := executor.getNextNonce()
|
||||
assert.Equal(t, uint64(10), nonce1)
|
||||
assert.Equal(t, uint64(11), executor.currentNonce)
|
||||
|
||||
// Get second nonce
|
||||
nonce2 := executor.getNextNonce()
|
||||
assert.Equal(t, uint64(11), nonce2)
|
||||
assert.Equal(t, uint64(12), executor.currentNonce)
|
||||
}
|
||||
|
||||
func TestExecutor_releaseNonce(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
currentNonce: 10,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Release current nonce - 1 (should work)
|
||||
executor.releaseNonce(9)
|
||||
assert.Equal(t, uint64(9), executor.currentNonce)
|
||||
|
||||
// Release older nonce (should not work)
|
||||
executor.currentNonce = 10
|
||||
executor.releaseNonce(5)
|
||||
assert.Equal(t, uint64(10), executor.currentNonce) // Should not change
|
||||
}
|
||||
|
||||
func TestExecutor_trackPendingTransaction(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
hash := common.HexToHash("0x123")
|
||||
nonce := uint64(5)
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp",
|
||||
InputAmount: big.NewInt(1e18),
|
||||
}
|
||||
|
||||
executor.trackPendingTransaction(nonce, hash, opp)
|
||||
|
||||
// Check transaction is tracked
|
||||
pending, exists := executor.nonceCache[nonce]
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, pending)
|
||||
assert.Equal(t, hash, pending.Hash)
|
||||
assert.Equal(t, nonce, pending.Nonce)
|
||||
assert.Equal(t, opp, pending.Opportunity)
|
||||
assert.False(t, pending.Confirmed)
|
||||
assert.False(t, pending.Failed)
|
||||
}
|
||||
|
||||
func TestExecutor_GetPendingTransactions(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Add pending transactions
|
||||
executor.nonceCache[1] = &PendingTransaction{
|
||||
Hash: common.HexToHash("0x01"),
|
||||
Confirmed: false,
|
||||
Failed: false,
|
||||
}
|
||||
executor.nonceCache[2] = &PendingTransaction{
|
||||
Hash: common.HexToHash("0x02"),
|
||||
Confirmed: true, // Already confirmed
|
||||
Failed: false,
|
||||
}
|
||||
executor.nonceCache[3] = &PendingTransaction{
|
||||
Hash: common.HexToHash("0x03"),
|
||||
Confirmed: false,
|
||||
Failed: false,
|
||||
}
|
||||
|
||||
pending := executor.GetPendingTransactions()
|
||||
|
||||
// Should only return unconfirmed, non-failed transactions
|
||||
assert.Len(t, pending, 2)
|
||||
}
|
||||
|
||||
func TestExecutor_Stop(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
stopCh: make(chan struct{}),
|
||||
stopped: false,
|
||||
}
|
||||
|
||||
executor.Stop()
|
||||
|
||||
assert.True(t, executor.stopped)
|
||||
|
||||
// Calling Stop again should not panic
|
||||
executor.Stop()
|
||||
assert.True(t, executor.stopped)
|
||||
}
|
||||
|
||||
func TestPendingTransaction_Fields(t *testing.T) {
|
||||
hash := common.HexToHash("0x123")
|
||||
nonce := uint64(5)
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp",
|
||||
}
|
||||
submittedAt := time.Now()
|
||||
|
||||
pending := &PendingTransaction{
|
||||
Hash: hash,
|
||||
Nonce: nonce,
|
||||
Opportunity: opp,
|
||||
SubmittedAt: submittedAt,
|
||||
LastChecked: submittedAt,
|
||||
Confirmed: false,
|
||||
Failed: false,
|
||||
FailReason: "",
|
||||
Receipt: nil,
|
||||
Retries: 0,
|
||||
}
|
||||
|
||||
assert.Equal(t, hash, pending.Hash)
|
||||
assert.Equal(t, nonce, pending.Nonce)
|
||||
assert.Equal(t, opp, pending.Opportunity)
|
||||
assert.Equal(t, submittedAt, pending.SubmittedAt)
|
||||
assert.False(t, pending.Confirmed)
|
||||
assert.False(t, pending.Failed)
|
||||
assert.Equal(t, 0, pending.Retries)
|
||||
}
|
||||
|
||||
func TestExecutionResult_Success(t *testing.T) {
|
||||
hash := common.HexToHash("0x123")
|
||||
actualProfit := big.NewInt(0.1e18)
|
||||
gasCost := big.NewInt(0.01e18)
|
||||
duration := 5 * time.Second
|
||||
|
||||
result := &ExecutionResult{
|
||||
Success: true,
|
||||
TxHash: hash,
|
||||
Receipt: nil,
|
||||
ActualProfit: actualProfit,
|
||||
GasCost: gasCost,
|
||||
Error: nil,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
assert.True(t, result.Success)
|
||||
assert.Equal(t, hash, result.TxHash)
|
||||
assert.Equal(t, actualProfit, result.ActualProfit)
|
||||
assert.Equal(t, gasCost, result.GasCost)
|
||||
assert.Nil(t, result.Error)
|
||||
assert.Equal(t, duration, result.Duration)
|
||||
}
|
||||
|
||||
func TestExecutionResult_Failure(t *testing.T) {
|
||||
hash := common.HexToHash("0x123")
|
||||
err := assert.AnError
|
||||
duration := 2 * time.Second
|
||||
|
||||
result := &ExecutionResult{
|
||||
Success: false,
|
||||
TxHash: hash,
|
||||
Receipt: nil,
|
||||
ActualProfit: nil,
|
||||
GasCost: nil,
|
||||
Error: err,
|
||||
Duration: duration,
|
||||
}
|
||||
|
||||
assert.False(t, result.Success)
|
||||
assert.Equal(t, hash, result.TxHash)
|
||||
assert.NotNil(t, result.Error)
|
||||
assert.Equal(t, duration, result.Duration)
|
||||
}
|
||||
|
||||
func TestExecutorConfig_RPC(t *testing.T) {
|
||||
config := &ExecutorConfig{
|
||||
PrivateKey: []byte{0x01, 0x02, 0x03},
|
||||
WalletAddress: common.HexToAddress("0x123"),
|
||||
RPCEndpoint: "http://localhost:8545",
|
||||
PrivateRPCEndpoint: "http://flashbots:8545",
|
||||
UsePrivateRPC: true,
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, config.PrivateKey)
|
||||
assert.NotEmpty(t, config.WalletAddress)
|
||||
assert.Equal(t, "http://localhost:8545", config.RPCEndpoint)
|
||||
assert.Equal(t, "http://flashbots:8545", config.PrivateRPCEndpoint)
|
||||
assert.True(t, config.UsePrivateRPC)
|
||||
}
|
||||
|
||||
func TestExecutorConfig_GasStrategy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strategy string
|
||||
multiplier float64
|
||||
maxIncrease float64
|
||||
}{
|
||||
{
|
||||
name: "Fast strategy",
|
||||
strategy: "fast",
|
||||
multiplier: 1.2,
|
||||
maxIncrease: 1.5,
|
||||
},
|
||||
{
|
||||
name: "Market strategy",
|
||||
strategy: "market",
|
||||
multiplier: 1.0,
|
||||
maxIncrease: 1.3,
|
||||
},
|
||||
{
|
||||
name: "Aggressive strategy",
|
||||
strategy: "aggressive",
|
||||
multiplier: 1.5,
|
||||
maxIncrease: 2.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config := &ExecutorConfig{
|
||||
GasPriceStrategy: tt.strategy,
|
||||
GasPriceMultiplier: tt.multiplier,
|
||||
MaxGasPriceIncrement: tt.maxIncrease,
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.strategy, config.GasPriceStrategy)
|
||||
assert.Equal(t, tt.multiplier, config.GasPriceMultiplier)
|
||||
assert.Equal(t, tt.maxIncrease, config.MaxGasPriceIncrement)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutor_calculateActualProfit(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Create mock receipt
|
||||
receipt := &types.Receipt{
|
||||
GasUsed: 150000,
|
||||
EffectiveGasPrice: big.NewInt(50e9), // 50 gwei
|
||||
}
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
GrossProfit: big.NewInt(0.2e18), // 0.2 ETH
|
||||
}
|
||||
|
||||
actualProfit := executor.calculateActualProfit(receipt, opp)
|
||||
|
||||
// Gas cost = 150000 * 50e9 = 0.0075 ETH
|
||||
// Actual profit = 0.2 - 0.0075 = 0.1925 ETH
|
||||
expectedProfit := big.NewInt(192500000000000000) // 0.1925 ETH
|
||||
assert.Equal(t, expectedProfit, actualProfit)
|
||||
}
|
||||
|
||||
func TestExecutor_NonceManagement_Concurrent(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
currentNonce: 10,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Simulate concurrent nonce requests
|
||||
nonces := make(chan uint64, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
nonce := executor.getNextNonce()
|
||||
nonces <- nonce
|
||||
}()
|
||||
}
|
||||
|
||||
// Collect all nonces
|
||||
receivedNonces := make(map[uint64]bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
nonce := <-nonces
|
||||
// Check for duplicates
|
||||
assert.False(t, receivedNonces[nonce], "Duplicate nonce detected")
|
||||
receivedNonces[nonce] = true
|
||||
}
|
||||
|
||||
// All nonces should be unique and sequential
|
||||
assert.Len(t, receivedNonces, 10)
|
||||
assert.Equal(t, uint64(20), executor.currentNonce)
|
||||
}
|
||||
|
||||
func TestExecutor_PendingTransaction_Timeout(t *testing.T) {
|
||||
submittedAt := time.Now().Add(-10 * time.Minute)
|
||||
|
||||
pending := &PendingTransaction{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
SubmittedAt: submittedAt,
|
||||
LastChecked: time.Now(),
|
||||
Confirmed: false,
|
||||
Failed: false,
|
||||
Retries: 0,
|
||||
}
|
||||
|
||||
timeout := 5 * time.Minute
|
||||
isTimedOut := time.Since(pending.SubmittedAt) > timeout
|
||||
|
||||
assert.True(t, isTimedOut)
|
||||
}
|
||||
|
||||
func TestExecutor_PendingTransaction_NotTimedOut(t *testing.T) {
|
||||
submittedAt := time.Now().Add(-2 * time.Minute)
|
||||
|
||||
pending := &PendingTransaction{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
SubmittedAt: submittedAt,
|
||||
LastChecked: time.Now(),
|
||||
Confirmed: false,
|
||||
Failed: false,
|
||||
Retries: 0,
|
||||
}
|
||||
|
||||
timeout := 5 * time.Minute
|
||||
isTimedOut := time.Since(pending.SubmittedAt) > timeout
|
||||
|
||||
assert.False(t, isTimedOut)
|
||||
}
|
||||
|
||||
func TestExecutor_PendingTransaction_MaxRetries(t *testing.T) {
|
||||
pending := &PendingTransaction{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
Retries: 3,
|
||||
}
|
||||
|
||||
maxRetries := 3
|
||||
canRetry := pending.Retries < maxRetries
|
||||
|
||||
assert.False(t, canRetry)
|
||||
}
|
||||
|
||||
func TestExecutor_PendingTransaction_CanRetry(t *testing.T) {
|
||||
pending := &PendingTransaction{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
Retries: 1,
|
||||
}
|
||||
|
||||
maxRetries := 3
|
||||
canRetry := pending.Retries < maxRetries
|
||||
|
||||
assert.True(t, canRetry)
|
||||
}
|
||||
|
||||
func TestExecutor_TransactionConfirmed(t *testing.T) {
|
||||
pending := &PendingTransaction{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
Confirmed: true,
|
||||
Failed: false,
|
||||
Receipt: &types.Receipt{Status: types.ReceiptStatusSuccessful},
|
||||
}
|
||||
|
||||
assert.True(t, pending.Confirmed)
|
||||
assert.False(t, pending.Failed)
|
||||
assert.NotNil(t, pending.Receipt)
|
||||
assert.Equal(t, types.ReceiptStatusSuccessful, pending.Receipt.Status)
|
||||
}
|
||||
|
||||
func TestExecutor_TransactionFailed(t *testing.T) {
|
||||
pending := &PendingTransaction{
|
||||
Hash: common.HexToHash("0x123"),
|
||||
Confirmed: true,
|
||||
Failed: true,
|
||||
FailReason: "transaction reverted",
|
||||
Receipt: &types.Receipt{Status: types.ReceiptStatusFailed},
|
||||
}
|
||||
|
||||
assert.True(t, pending.Confirmed)
|
||||
assert.True(t, pending.Failed)
|
||||
assert.Equal(t, "transaction reverted", pending.FailReason)
|
||||
assert.NotNil(t, pending.Receipt)
|
||||
assert.Equal(t, types.ReceiptStatusFailed, pending.Receipt.Status)
|
||||
}
|
||||
|
||||
func TestExecutorConfig_Defaults(t *testing.T) {
|
||||
config := DefaultExecutorConfig()
|
||||
|
||||
// Test all default values
|
||||
assert.Equal(t, uint64(1), config.ConfirmationBlocks)
|
||||
assert.Equal(t, 5*time.Minute, config.TimeoutPerTx)
|
||||
assert.Equal(t, 3, config.MaxRetries)
|
||||
assert.Equal(t, 5*time.Second, config.RetryDelay)
|
||||
assert.Equal(t, uint64(2), config.NonceMargin)
|
||||
assert.Equal(t, "fast", config.GasPriceStrategy)
|
||||
assert.Equal(t, float64(1.1), config.GasPriceMultiplier)
|
||||
assert.Equal(t, float64(1.5), config.MaxGasPriceIncrement)
|
||||
assert.Equal(t, 1*time.Second, config.MonitorInterval)
|
||||
assert.Equal(t, 1*time.Minute, config.CleanupInterval)
|
||||
}
|
||||
|
||||
func TestExecutor_MultipleOpportunities(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
currentNonce: 10,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Track multiple opportunities
|
||||
for i := 0; i < 5; i++ {
|
||||
hash := common.HexToHash(string(rune(i)))
|
||||
nonce := executor.getNextNonce()
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: string(rune(i)),
|
||||
}
|
||||
|
||||
executor.trackPendingTransaction(nonce, hash, opp)
|
||||
}
|
||||
|
||||
// Check all are tracked
|
||||
assert.Len(t, executor.nonceCache, 5)
|
||||
assert.Equal(t, uint64(15), executor.currentNonce)
|
||||
|
||||
// Get pending transactions
|
||||
pending := executor.GetPendingTransactions()
|
||||
assert.Len(t, pending, 5)
|
||||
}
|
||||
|
||||
func TestExecutor_CleanupOldTransactions(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Add old confirmed transaction
|
||||
oldTime := time.Now().Add(-2 * time.Hour)
|
||||
executor.nonceCache[1] = &PendingTransaction{
|
||||
Hash: common.HexToHash("0x01"),
|
||||
Confirmed: true,
|
||||
LastChecked: oldTime,
|
||||
}
|
||||
|
||||
// Add recent transaction
|
||||
executor.nonceCache[2] = &PendingTransaction{
|
||||
Hash: common.HexToHash("0x02"),
|
||||
Confirmed: false,
|
||||
LastChecked: time.Now(),
|
||||
}
|
||||
|
||||
// Simulate cleanup (cutoff = 1 hour)
|
||||
cutoff := time.Now().Add(-1 * time.Hour)
|
||||
for nonce, pending := range executor.nonceCache {
|
||||
if (pending.Confirmed || pending.Failed) && pending.LastChecked.Before(cutoff) {
|
||||
delete(executor.nonceCache, nonce)
|
||||
}
|
||||
}
|
||||
|
||||
// Only recent transaction should remain
|
||||
assert.Len(t, executor.nonceCache, 1)
|
||||
_, exists := executor.nonceCache[2]
|
||||
assert.True(t, exists)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkExecutor_getNextNonce(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
currentNonce: 0,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = executor.getNextNonce()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecutor_trackPendingTransaction(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
hash := common.HexToHash("0x123")
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
executor.trackPendingTransaction(uint64(i), hash, opp)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExecutor_GetPendingTransactions(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
executor := &Executor{
|
||||
logger: logger,
|
||||
nonceCache: make(map[uint64]*PendingTransaction),
|
||||
}
|
||||
|
||||
// Add 100 pending transactions
|
||||
for i := 0; i < 100; i++ {
|
||||
executor.nonceCache[uint64(i)] = &PendingTransaction{
|
||||
Hash: common.HexToHash(string(rune(i))),
|
||||
Confirmed: false,
|
||||
Failed: false,
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = executor.GetPendingTransactions()
|
||||
}
|
||||
}
|
||||
482
pkg/execution/flashloan_test.go
Normal file
482
pkg/execution/flashloan_test.go
Normal file
@@ -0,0 +1,482 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
633
pkg/execution/risk_manager_test.go
Normal file
633
pkg/execution/risk_manager_test.go
Normal file
@@ -0,0 +1,633 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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 TestDefaultRiskManagerConfig(t *testing.T) {
|
||||
config := DefaultRiskManagerConfig()
|
||||
|
||||
assert.NotNil(t, config)
|
||||
assert.True(t, config.Enabled)
|
||||
assert.NotNil(t, config.MaxPositionSize)
|
||||
assert.NotNil(t, config.MaxDailyVolume)
|
||||
assert.NotNil(t, config.MinProfitThreshold)
|
||||
assert.Equal(t, float64(0.01), config.MinROI)
|
||||
assert.Equal(t, uint16(200), config.MaxSlippageBPS)
|
||||
assert.Equal(t, uint64(5), config.MaxConcurrentTxs)
|
||||
}
|
||||
|
||||
func TestNewRiskManager(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
assert.NotNil(t, manager)
|
||||
assert.NotNil(t, manager.config)
|
||||
assert.NotNil(t, manager.activeTxs)
|
||||
assert.NotNil(t, manager.dailyVolume)
|
||||
assert.NotNil(t, manager.recentFailures)
|
||||
assert.False(t, manager.circuitBreakerOpen)
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_Success(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false // Disable simulation for unit test
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1, // 10%
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9), // 50 gwei
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9), // 2 gwei
|
||||
GasLimit: 180000,
|
||||
Slippage: 50, // 0.5%
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, assessment)
|
||||
assert.True(t, assessment.Approved)
|
||||
assert.Empty(t, assessment.Warnings)
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_CircuitBreakerOpen(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Open circuit breaker
|
||||
manager.openCircuitBreaker()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "circuit breaker")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_PositionSizeExceeded(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Create opportunity with amount exceeding max position size
|
||||
largeAmount := new(big.Int).Add(config.MaxPositionSize, big.NewInt(1))
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: largeAmount,
|
||||
OutputAmount: new(big.Int).Mul(largeAmount, big.NewInt(11)),
|
||||
NetProfit: big.NewInt(1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "position size")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_DailyVolumeExceeded(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Set daily volume to max
|
||||
manager.dailyVolume = config.MaxDailyVolume
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "daily volume")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_GasPriceTooHigh(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
// Set gas price above max
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: new(big.Int).Add(config.MaxGasPrice, big.NewInt(1)),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "gas price")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_ProfitTooLow(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Profit below threshold
|
||||
lowProfit := new(big.Int).Sub(config.MinProfitThreshold, big.NewInt(1))
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: new(big.Int).Add(big.NewInt(1e18), lowProfit),
|
||||
NetProfit: lowProfit,
|
||||
ROI: 0.00001,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "profit")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_ROITooLow(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.005e18),
|
||||
NetProfit: big.NewInt(0.005e18), // 0.5% ROI, below 1% threshold
|
||||
ROI: 0.005,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "ROI")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_SlippageTooHigh(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 300, // 3%, above 2% max
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "slippage")
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_ConcurrentLimitExceeded(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
config.MaxConcurrentTxs = 2
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Add max concurrent transactions
|
||||
manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{}
|
||||
manager.activeTxs[common.HexToHash("0x02")] = &ActiveTransaction{}
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, assessment.Approved)
|
||||
assert.Contains(t, assessment.Reason, "concurrent")
|
||||
}
|
||||
|
||||
func TestRiskManager_TrackTransaction(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
hash := common.HexToHash("0x123")
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
}
|
||||
gasPrice := big.NewInt(50e9)
|
||||
|
||||
manager.TrackTransaction(hash, opp, gasPrice)
|
||||
|
||||
// Check transaction is tracked
|
||||
manager.mu.RLock()
|
||||
tx, exists := manager.activeTxs[hash]
|
||||
manager.mu.RUnlock()
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, tx)
|
||||
assert.Equal(t, hash, tx.Hash)
|
||||
assert.Equal(t, opp.InputAmount, tx.Amount)
|
||||
assert.Equal(t, gasPrice, tx.GasPrice)
|
||||
}
|
||||
|
||||
func TestRiskManager_UntrackTransaction(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
hash := common.HexToHash("0x123")
|
||||
manager.activeTxs[hash] = &ActiveTransaction{Hash: hash}
|
||||
|
||||
manager.UntrackTransaction(hash)
|
||||
|
||||
// Check transaction is no longer tracked
|
||||
manager.mu.RLock()
|
||||
_, exists := manager.activeTxs[hash]
|
||||
manager.mu.RUnlock()
|
||||
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestRiskManager_RecordFailure(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.CircuitBreakerFailures = 3
|
||||
config.CircuitBreakerWindow = 1 * time.Minute
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
hash := common.HexToHash("0x123")
|
||||
|
||||
// Record failures below threshold
|
||||
manager.RecordFailure(hash, "test failure 1")
|
||||
assert.False(t, manager.circuitBreakerOpen)
|
||||
|
||||
manager.RecordFailure(hash, "test failure 2")
|
||||
assert.False(t, manager.circuitBreakerOpen)
|
||||
|
||||
// Third failure should open circuit breaker
|
||||
manager.RecordFailure(hash, "test failure 3")
|
||||
assert.True(t, manager.circuitBreakerOpen)
|
||||
}
|
||||
|
||||
func TestRiskManager_RecordSuccess(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
hash := common.HexToHash("0x123")
|
||||
actualProfit := big.NewInt(0.1e18)
|
||||
|
||||
manager.RecordSuccess(hash, actualProfit)
|
||||
|
||||
// Check that recent failures were cleared
|
||||
assert.Empty(t, manager.recentFailures)
|
||||
}
|
||||
|
||||
func TestRiskManager_CircuitBreaker_Cooldown(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.CircuitBreakerCooldown = 1 * time.Millisecond
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Open circuit breaker
|
||||
manager.openCircuitBreaker()
|
||||
assert.True(t, manager.circuitBreakerOpen)
|
||||
|
||||
// Wait for cooldown
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
// Circuit breaker should be closed after cooldown
|
||||
assert.False(t, manager.checkCircuitBreaker())
|
||||
}
|
||||
|
||||
func TestRiskManager_checkConcurrentLimit(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MaxConcurrentTxs = 3
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Add transactions below limit
|
||||
manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{}
|
||||
manager.activeTxs[common.HexToHash("0x02")] = &ActiveTransaction{}
|
||||
|
||||
assert.True(t, manager.checkConcurrentLimit())
|
||||
|
||||
// Add transaction at limit
|
||||
manager.activeTxs[common.HexToHash("0x03")] = &ActiveTransaction{}
|
||||
|
||||
assert.False(t, manager.checkConcurrentLimit())
|
||||
}
|
||||
|
||||
func TestRiskManager_checkPositionSize(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MaxPositionSize = big.NewInt(10e18)
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
assert.True(t, manager.checkPositionSize(big.NewInt(5e18)))
|
||||
assert.True(t, manager.checkPositionSize(big.NewInt(10e18)))
|
||||
assert.False(t, manager.checkPositionSize(big.NewInt(11e18)))
|
||||
}
|
||||
|
||||
func TestRiskManager_checkDailyVolume(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MaxDailyVolume = big.NewInt(100e18)
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
manager.dailyVolume = big.NewInt(90e18)
|
||||
|
||||
assert.True(t, manager.checkDailyVolume(big.NewInt(5e18)))
|
||||
assert.True(t, manager.checkDailyVolume(big.NewInt(10e18)))
|
||||
assert.False(t, manager.checkDailyVolume(big.NewInt(15e18)))
|
||||
}
|
||||
|
||||
func TestRiskManager_updateDailyVolume(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
initialVolume := big.NewInt(10e18)
|
||||
manager.dailyVolume = initialVolume
|
||||
|
||||
addAmount := big.NewInt(5e18)
|
||||
manager.updateDailyVolume(addAmount)
|
||||
|
||||
expectedVolume := new(big.Int).Add(initialVolume, addAmount)
|
||||
assert.Equal(t, expectedVolume, manager.dailyVolume)
|
||||
}
|
||||
|
||||
func TestRiskManager_checkGasPrice(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MaxGasPrice = big.NewInt(100e9) // 100 gwei
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
assert.True(t, manager.checkGasPrice(big.NewInt(50e9)))
|
||||
assert.True(t, manager.checkGasPrice(big.NewInt(100e9)))
|
||||
assert.False(t, manager.checkGasPrice(big.NewInt(101e9)))
|
||||
}
|
||||
|
||||
func TestRiskManager_checkGasCost(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MaxGasCost = big.NewInt(0.1e18) // 0.1 ETH
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
assert.True(t, manager.checkGasCost(big.NewInt(0.05e18)))
|
||||
assert.True(t, manager.checkGasCost(big.NewInt(0.1e18)))
|
||||
assert.False(t, manager.checkGasCost(big.NewInt(0.11e18)))
|
||||
}
|
||||
|
||||
func TestRiskManager_checkMinProfit(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MinProfitThreshold = big.NewInt(0.01e18) // 0.01 ETH
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
assert.False(t, manager.checkMinProfit(big.NewInt(0.005e18)))
|
||||
assert.True(t, manager.checkMinProfit(big.NewInt(0.01e18)))
|
||||
assert.True(t, manager.checkMinProfit(big.NewInt(0.02e18)))
|
||||
}
|
||||
|
||||
func TestRiskManager_checkMinROI(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MinROI = 0.01 // 1%
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
assert.False(t, manager.checkMinROI(0.005)) // 0.5%
|
||||
assert.True(t, manager.checkMinROI(0.01)) // 1%
|
||||
assert.True(t, manager.checkMinROI(0.02)) // 2%
|
||||
}
|
||||
|
||||
func TestRiskManager_checkSlippage(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.MaxSlippageBPS = 200 // 2%
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
assert.True(t, manager.checkSlippage(100)) // 1%
|
||||
assert.True(t, manager.checkSlippage(200)) // 2%
|
||||
assert.False(t, manager.checkSlippage(300)) // 3%
|
||||
}
|
||||
|
||||
func TestRiskManager_GetActiveTransactions(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
// Add some active transactions
|
||||
manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{Hash: common.HexToHash("0x01")}
|
||||
manager.activeTxs[common.HexToHash("0x02")] = &ActiveTransaction{Hash: common.HexToHash("0x02")}
|
||||
|
||||
activeTxs := manager.GetActiveTransactions()
|
||||
|
||||
assert.Len(t, activeTxs, 2)
|
||||
}
|
||||
|
||||
func TestRiskManager_GetStats(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
// Add some state
|
||||
manager.activeTxs[common.HexToHash("0x01")] = &ActiveTransaction{}
|
||||
manager.dailyVolume = big.NewInt(50e18)
|
||||
manager.circuitBreakerOpen = true
|
||||
|
||||
stats := manager.GetStats()
|
||||
|
||||
assert.NotNil(t, stats)
|
||||
assert.Equal(t, 1, stats["active_transactions"])
|
||||
assert.Equal(t, "50000000000000000000", stats["daily_volume"])
|
||||
assert.Equal(t, true, stats["circuit_breaker_open"])
|
||||
}
|
||||
|
||||
func TestRiskManager_AssessRisk_WithWarnings(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
// Create opportunity with high gas cost (should generate warning)
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 2000000, // Very high gas
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 2400000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
assessment, err := manager.AssessRisk(context.Background(), opp, tx)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, assessment.Approved) // Should still be approved
|
||||
assert.NotEmpty(t, assessment.Warnings) // But with warnings
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkRiskManager_AssessRisk(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
config := DefaultRiskManagerConfig()
|
||||
config.SimulationEnabled = false
|
||||
|
||||
manager := NewRiskManager(config, nil, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputAmount: big.NewInt(1.1e18),
|
||||
NetProfit: big.NewInt(0.1e18),
|
||||
ROI: 0.1,
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
tx := &SwapTransaction{
|
||||
MaxFeePerGas: big.NewInt(50e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
GasLimit: 180000,
|
||||
Slippage: 50,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = manager.AssessRisk(context.Background(), opp, tx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRiskManager_checkCircuitBreaker(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
manager := NewRiskManager(nil, nil, logger)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = manager.checkCircuitBreaker()
|
||||
}
|
||||
}
|
||||
560
pkg/execution/transaction_builder_test.go
Normal file
560
pkg/execution/transaction_builder_test.go
Normal file
@@ -0,0 +1,560 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/your-org/mev-bot/pkg/arbitrage"
|
||||
mevtypes "github.com/your-org/mev-bot/pkg/types"
|
||||
)
|
||||
|
||||
func TestDefaultTransactionBuilderConfig(t *testing.T) {
|
||||
config := DefaultTransactionBuilderConfig()
|
||||
|
||||
assert.NotNil(t, config)
|
||||
assert.Equal(t, uint16(50), config.DefaultSlippageBPS)
|
||||
assert.Equal(t, uint16(300), config.MaxSlippageBPS)
|
||||
assert.Equal(t, float64(1.2), config.GasLimitMultiplier)
|
||||
assert.Equal(t, uint64(3000000), config.MaxGasLimit)
|
||||
assert.Equal(t, 5*time.Minute, config.DefaultDeadline)
|
||||
}
|
||||
|
||||
func TestNewTransactionBuilder(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161) // Arbitrum
|
||||
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
assert.NotNil(t, builder)
|
||||
assert.NotNil(t, builder.config)
|
||||
assert.Equal(t, chainID, builder.chainID)
|
||||
assert.NotNil(t, builder.uniswapV2Encoder)
|
||||
assert.NotNil(t, builder.uniswapV3Encoder)
|
||||
assert.NotNil(t, builder.curveEncoder)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_SingleSwap_UniswapV2(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-1",
|
||||
Type: arbitrage.OpportunityTypeTwoPool,
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
OutputAmount: big.NewInt(1500e6),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, tx)
|
||||
assert.NotEmpty(t, tx.To)
|
||||
assert.NotEmpty(t, tx.Data)
|
||||
assert.NotNil(t, tx.Value)
|
||||
assert.Greater(t, tx.GasLimit, uint64(0))
|
||||
assert.NotNil(t, tx.MaxFeePerGas)
|
||||
assert.NotNil(t, tx.MaxPriorityFeePerGas)
|
||||
assert.NotNil(t, tx.MinOutput)
|
||||
assert.False(t, tx.RequiresFlashloan)
|
||||
assert.Equal(t, uint16(50), tx.Slippage) // Default slippage
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_SingleSwap_UniswapV3(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-2",
|
||||
Type: arbitrage.OpportunityTypeTwoPool,
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
OutputAmount: big.NewInt(1500e6),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV3,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
Fee: 3000, // 0.3%
|
||||
},
|
||||
},
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, tx)
|
||||
assert.NotEmpty(t, tx.To)
|
||||
assert.NotEmpty(t, tx.Data)
|
||||
assert.Equal(t, UniswapV3SwapRouterAddress, tx.To)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_MultiHop_UniswapV2(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-3",
|
||||
Type: arbitrage.OpportunityTypeMultiHop,
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputToken: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
OutputAmount: big.NewInt(1e7),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
AmountIn: big.NewInt(1500e6),
|
||||
AmountOut: big.NewInt(1e7),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"),
|
||||
},
|
||||
},
|
||||
EstimatedGas: 250000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, tx)
|
||||
assert.NotEmpty(t, tx.To)
|
||||
assert.NotEmpty(t, tx.Data)
|
||||
assert.Equal(t, UniswapV2RouterAddress, tx.To)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_MultiHop_UniswapV3(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-4",
|
||||
Type: arbitrage.OpportunityTypeMultiHop,
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputToken: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
OutputAmount: big.NewInt(1e7),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV3,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
Fee: 3000,
|
||||
},
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV3,
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
AmountIn: big.NewInt(1500e6),
|
||||
AmountOut: big.NewInt(1e7),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"),
|
||||
Fee: 500,
|
||||
},
|
||||
},
|
||||
EstimatedGas: 250000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, tx)
|
||||
assert.Equal(t, UniswapV3SwapRouterAddress, tx.To)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_Curve(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-5",
|
||||
Type: arbitrage.OpportunityTypeTwoPool,
|
||||
InputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
InputAmount: big.NewInt(1500e6),
|
||||
OutputToken: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
|
||||
OutputAmount: big.NewInt(1500e6),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolCurve,
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
|
||||
AmountIn: big.NewInt(1500e6),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
EstimatedGas: 200000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, tx)
|
||||
// For Curve, tx.To should be the pool address
|
||||
assert.Equal(t, opp.Path[0].PoolAddress, tx.To)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_EmptyPath(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-6",
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
Path: []arbitrage.SwapStep{},
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
_, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "empty swap path")
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_BuildTransaction_UnsupportedProtocol(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-7",
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: "unknown_protocol",
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
_, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported protocol")
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_calculateMinOutput(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
outputAmount *big.Int
|
||||
slippageBPS uint16
|
||||
expectedMin *big.Int
|
||||
}{
|
||||
{
|
||||
name: "0.5% slippage",
|
||||
outputAmount: big.NewInt(1000e6),
|
||||
slippageBPS: 50,
|
||||
expectedMin: big.NewInt(995e6), // 0.5% less
|
||||
},
|
||||
{
|
||||
name: "1% slippage",
|
||||
outputAmount: big.NewInt(1000e6),
|
||||
slippageBPS: 100,
|
||||
expectedMin: big.NewInt(990e6), // 1% less
|
||||
},
|
||||
{
|
||||
name: "3% slippage",
|
||||
outputAmount: big.NewInt(1000e6),
|
||||
slippageBPS: 300,
|
||||
expectedMin: big.NewInt(970e6), // 3% less
|
||||
},
|
||||
{
|
||||
name: "Zero slippage",
|
||||
outputAmount: big.NewInt(1000e6),
|
||||
slippageBPS: 0,
|
||||
expectedMin: big.NewInt(1000e6), // No change
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
minOutput := builder.calculateMinOutput(tt.outputAmount, tt.slippageBPS)
|
||||
assert.Equal(t, tt.expectedMin, minOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_calculateGasLimit(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
estimatedGas uint64
|
||||
expectedMin uint64
|
||||
expectedMax uint64
|
||||
}{
|
||||
{
|
||||
name: "Normal gas estimate",
|
||||
estimatedGas: 150000,
|
||||
expectedMin: 180000, // 150k * 1.2
|
||||
expectedMax: 180001,
|
||||
},
|
||||
{
|
||||
name: "High gas estimate",
|
||||
estimatedGas: 2500000,
|
||||
expectedMin: 3000000, // Capped at max
|
||||
expectedMax: 3000000,
|
||||
},
|
||||
{
|
||||
name: "Zero gas estimate",
|
||||
estimatedGas: 0,
|
||||
expectedMin: 0, // 0 * 1.2
|
||||
expectedMax: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gasLimit := builder.calculateGasLimit(tt.estimatedGas)
|
||||
assert.GreaterOrEqual(t, gasLimit, tt.expectedMin)
|
||||
assert.LessOrEqual(t, gasLimit, tt.expectedMax)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_SignTransaction(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
// Create a test private key
|
||||
privateKey, err := crypto.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := &SwapTransaction{
|
||||
To: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
Data: []byte{0x01, 0x02, 0x03, 0x04},
|
||||
Value: big.NewInt(0),
|
||||
GasLimit: 180000,
|
||||
MaxFeePerGas: big.NewInt(100e9), // 100 gwei
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9), // 2 gwei
|
||||
}
|
||||
|
||||
nonce := uint64(5)
|
||||
|
||||
signedTx, err := builder.SignTransaction(tx, nonce, crypto.FromECDSA(privateKey))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, signedTx)
|
||||
assert.Equal(t, nonce, signedTx.Nonce())
|
||||
assert.Equal(t, tx.To, *signedTx.To())
|
||||
assert.Equal(t, tx.GasLimit, signedTx.Gas())
|
||||
assert.Equal(t, tx.MaxFeePerGas, signedTx.GasFeeCap())
|
||||
assert.Equal(t, tx.MaxPriorityFeePerGas, signedTx.GasTipCap())
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_SignTransaction_InvalidKey(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
tx := &SwapTransaction{
|
||||
To: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
Data: []byte{0x01, 0x02, 0x03, 0x04},
|
||||
Value: big.NewInt(0),
|
||||
GasLimit: 180000,
|
||||
MaxFeePerGas: big.NewInt(100e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
}
|
||||
|
||||
nonce := uint64(5)
|
||||
invalidKey := []byte{0x01, 0x02, 0x03} // Too short
|
||||
|
||||
_, err := builder.SignTransaction(tx, nonce, invalidKey)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_CustomSlippage(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
|
||||
config := DefaultTransactionBuilderConfig()
|
||||
config.DefaultSlippageBPS = 100 // 1% slippage
|
||||
|
||||
builder := NewTransactionBuilder(config, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-8",
|
||||
Type: arbitrage.OpportunityTypeTwoPool,
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
OutputAmount: big.NewInt(1000e6),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1000e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint16(100), tx.Slippage)
|
||||
// MinOutput should be 990e6 (1% slippage on 1000e6)
|
||||
assert.Equal(t, big.NewInt(990e6), tx.MinOutput)
|
||||
}
|
||||
|
||||
func TestTransactionBuilder_ZeroAmounts(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "test-opp-9",
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(0),
|
||||
OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
OutputAmount: big.NewInt(0),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(0),
|
||||
AmountOut: big.NewInt(0),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
tx, err := builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, big.NewInt(0), tx.MinOutput)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkTransactionBuilder_BuildTransaction(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
ID: "bench-opp",
|
||||
Type: arbitrage.OpportunityTypeTwoPool,
|
||||
InputToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
InputAmount: big.NewInt(1e18),
|
||||
OutputToken: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
OutputAmount: big.NewInt(1500e6),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
Protocol: mevtypes.ProtocolUniswapV2,
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
AmountIn: big.NewInt(1e18),
|
||||
AmountOut: big.NewInt(1500e6),
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
EstimatedGas: 150000,
|
||||
}
|
||||
|
||||
fromAddress := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = builder.BuildTransaction(context.Background(), opp, fromAddress)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTransactionBuilder_SignTransaction(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
chainID := big.NewInt(42161)
|
||||
builder := NewTransactionBuilder(nil, chainID, logger)
|
||||
|
||||
privateKey, _ := crypto.GenerateKey()
|
||||
|
||||
tx := &SwapTransaction{
|
||||
To: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
Data: []byte{0x01, 0x02, 0x03, 0x04},
|
||||
Value: big.NewInt(0),
|
||||
GasLimit: 180000,
|
||||
MaxFeePerGas: big.NewInt(100e9),
|
||||
MaxPriorityFeePerGas: big.NewInt(2e9),
|
||||
}
|
||||
|
||||
nonce := uint64(5)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = builder.SignTransaction(tx, nonce, crypto.FromECDSA(privateKey))
|
||||
}
|
||||
}
|
||||
305
pkg/execution/uniswap_v2_encoder_test.go
Normal file
305
pkg/execution/uniswap_v2_encoder_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewUniswapV2Encoder(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
assert.NotNil(t, encoder)
|
||||
assert.Equal(t, UniswapV2RouterAddress, encoder.routerAddress)
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_EncodeSwap(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") // WETH
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8") // USDC
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.routerAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID (first 4 bytes)
|
||||
// swapExactTokensForTokens(uint256,uint256,address[],address,uint256)
|
||||
assert.Len(t, data, 4+5*32+32+2*32) // methodID + 5 params + array length + 2 addresses
|
||||
|
||||
// Verify method signature
|
||||
expectedMethodID := []byte{0x38, 0xed, 0x17, 0x39} // swapExactTokensForTokens signature
|
||||
assert.Equal(t, expectedMethodID, data[:4])
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_EncodeMultiHopSwap(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
path := []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"), // WETH
|
||||
common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"), // USDC
|
||||
common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"), // WBTC
|
||||
}
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1e7)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeMultiHopSwap(
|
||||
path,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.routerAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Verify method ID
|
||||
expectedMethodID := []byte{0x38, 0xed, 0x17, 0x39}
|
||||
assert.Equal(t, expectedMethodID, data[:4])
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_EncodeMultiHopSwap_EmptyPath(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
path := []common.Address{}
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1e7)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
_, _, err := encoder.EncodeMultiHopSwap(
|
||||
path,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path must contain at least 2 tokens")
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_EncodeMultiHopSwap_SingleToken(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
path := []common.Address{
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
}
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1e7)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
_, _, err := encoder.EncodeMultiHopSwap(
|
||||
path,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path must contain at least 2 tokens")
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_EncodeExactOutput(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountOut := big.NewInt(1500e6)
|
||||
maxAmountIn := big.NewInt(2e18)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeExactOutput(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountOut,
|
||||
maxAmountIn,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.routerAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Verify method ID for swapTokensForExactTokens
|
||||
assert.Len(t, data, 4+5*32+32+2*32)
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_ZeroAddresses(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
tokenIn := common.Address{}
|
||||
tokenOut := common.Address{}
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.Address{}
|
||||
recipient := common.Address{}
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
// Should not error with zero addresses (validation done elsewhere)
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_ZeroAmounts(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(0)
|
||||
minAmountOut := big.NewInt(0)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
// Should not error with zero amounts (validation done elsewhere)
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_LargeAmounts(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
|
||||
// Max uint256
|
||||
amountIn := new(big.Int)
|
||||
amountIn.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10)
|
||||
minAmountOut := new(big.Int)
|
||||
minAmountOut.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10)
|
||||
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV2Encoder_PastDeadline(t *testing.T) {
|
||||
encoder := NewUniswapV2Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(-5 * time.Minute) // Past deadline
|
||||
|
||||
// Should not error (validation done on-chain)
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestPadLeft(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
length int
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "Empty input",
|
||||
input: []byte{},
|
||||
length: 32,
|
||||
expected: 32,
|
||||
},
|
||||
{
|
||||
name: "Small number",
|
||||
input: []byte{0x01},
|
||||
length: 32,
|
||||
expected: 32,
|
||||
},
|
||||
{
|
||||
name: "Full size",
|
||||
input: make([]byte, 32),
|
||||
length: 32,
|
||||
expected: 32,
|
||||
},
|
||||
{
|
||||
name: "Address",
|
||||
input: make([]byte, 20),
|
||||
length: 32,
|
||||
expected: 32,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := padLeft(tt.input, tt.length)
|
||||
assert.Len(t, result, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
484
pkg/execution/uniswap_v3_encoder_test.go
Normal file
484
pkg/execution/uniswap_v3_encoder_test.go
Normal file
@@ -0,0 +1,484 @@
|
||||
package execution
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/your-org/mev-bot/pkg/arbitrage"
|
||||
"github.com/your-org/mev-bot/pkg/cache"
|
||||
)
|
||||
|
||||
func TestNewUniswapV3Encoder(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
assert.NotNil(t, encoder)
|
||||
assert.Equal(t, UniswapV3SwapRouterAddress, encoder.swapRouterAddress)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeSwap(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1") // WETH
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8") // USDC
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
fee := uint32(3000) // 0.3%
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
fee,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Check method ID (first 4 bytes)
|
||||
// exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))
|
||||
assert.GreaterOrEqual(t, len(data), 4)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeMultiHopSwap(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
Fee: 3000,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
Fee: 3000,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000003")
|
||||
minAmountOut := big.NewInt(1e7)
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeMultiHopSwap(
|
||||
opp,
|
||||
recipient,
|
||||
minAmountOut,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Verify method ID for exactInput
|
||||
assert.GreaterOrEqual(t, len(data), 4)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeMultiHopSwap_SingleStep(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
Fee: 3000,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000003")
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
_, _, err := encoder.EncodeMultiHopSwap(
|
||||
opp,
|
||||
recipient,
|
||||
minAmountOut,
|
||||
deadline,
|
||||
)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "multi-hop requires at least 2 steps")
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeMultiHopSwap_EmptyPath(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
Path: []arbitrage.SwapStep{},
|
||||
}
|
||||
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000003")
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
_, _, err := encoder.EncodeMultiHopSwap(
|
||||
opp,
|
||||
recipient,
|
||||
minAmountOut,
|
||||
deadline,
|
||||
)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "multi-hop requires at least 2 steps")
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_buildEncodedPath(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
Fee: 3000,
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
Fee: 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
path := encoder.buildEncodedPath(opp)
|
||||
|
||||
// Path should be: token (20) + fee (3) + token (20) + fee (3) + token (20) = 66 bytes
|
||||
assert.Len(t, path, 66)
|
||||
|
||||
// First 20 bytes should be first token
|
||||
assert.Equal(t, opp.Path[0].TokenIn.Bytes(), path[:20])
|
||||
|
||||
// Bytes 20-23 should be first fee (3000 = 0x000BB8)
|
||||
assert.Equal(t, []byte{0x00, 0x0B, 0xB8}, path[20:23])
|
||||
|
||||
// Bytes 23-43 should be second token
|
||||
assert.Equal(t, opp.Path[0].TokenOut.Bytes(), path[23:43])
|
||||
|
||||
// Bytes 43-46 should be second fee (500 = 0x0001F4)
|
||||
assert.Equal(t, []byte{0x00, 0x01, 0xF4}, path[43:46])
|
||||
|
||||
// Bytes 46-66 should be third token
|
||||
assert.Equal(t, opp.Path[1].TokenOut.Bytes(), path[46:66])
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_buildEncodedPath_SingleStep(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
Fee: 3000,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
path := encoder.buildEncodedPath(opp)
|
||||
|
||||
// Path should be: token (20) + fee (3) + token (20) = 43 bytes
|
||||
assert.Len(t, path, 43)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeExactOutput(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountOut := big.NewInt(1500e6)
|
||||
maxAmountIn := big.NewInt(2e18)
|
||||
fee := uint32(3000)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeExactOutput(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountOut,
|
||||
maxAmountIn,
|
||||
fee,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
assert.GreaterOrEqual(t, len(data), 4)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeMulticall(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
call1 := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
call2 := []byte{0x05, 0x06, 0x07, 0x08}
|
||||
calls := [][]byte{call1, call2}
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeMulticall(calls, deadline)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
assert.GreaterOrEqual(t, len(data), 4)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeMulticall_EmptyCalls(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
calls := [][]byte{}
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeMulticall(calls, deadline)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_EncodeMulticall_SingleCall(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
call := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
calls := [][]byte{call}
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeMulticall(calls, deadline)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_DifferentFees(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
fees := []uint32{
|
||||
100, // 0.01%
|
||||
500, // 0.05%
|
||||
3000, // 0.3%
|
||||
10000, // 1%
|
||||
}
|
||||
|
||||
for _, fee := range fees {
|
||||
t.Run(string(rune(fee)), func(t *testing.T) {
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
fee,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_ZeroAmounts(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(0)
|
||||
minAmountOut := big.NewInt(0)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
fee := uint32(3000)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
fee,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_LargeAmounts(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
|
||||
// Max uint256
|
||||
amountIn := new(big.Int)
|
||||
amountIn.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10)
|
||||
minAmountOut := new(big.Int)
|
||||
minAmountOut.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10)
|
||||
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
fee := uint32(3000)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
fee,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, to)
|
||||
assert.NotEmpty(t, data)
|
||||
}
|
||||
|
||||
func TestUniswapV3Encoder_LongPath(t *testing.T) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
// Create a 5-hop path
|
||||
opp := &arbitrage.Opportunity{
|
||||
InputAmount: big.NewInt(1e18),
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
Fee: 3000,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000001"),
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
Fee: 500,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000002"),
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
TokenOut: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
|
||||
Fee: 3000,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000003"),
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
|
||||
TokenOut: common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"),
|
||||
Fee: 500,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000004"),
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"),
|
||||
TokenOut: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
Fee: 3000,
|
||||
PoolAddress: common.HexToAddress("0x0000000000000000000000000000000000000005"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000003")
|
||||
minAmountOut := big.NewInt(1e7)
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
to, data, err := encoder.EncodeMultiHopSwap(
|
||||
opp,
|
||||
recipient,
|
||||
minAmountOut,
|
||||
deadline,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, encoder.swapRouterAddress, to)
|
||||
assert.NotEmpty(t, data)
|
||||
|
||||
// Path should be: 20 + (23 * 5) = 135 bytes
|
||||
path := encoder.buildEncodedPath(opp)
|
||||
assert.Len(t, path, 135)
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkUniswapV3Encoder_EncodeSwap(b *testing.B) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
tokenIn := common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1")
|
||||
tokenOut := common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8")
|
||||
amountIn := big.NewInt(1e18)
|
||||
minAmountOut := big.NewInt(1500e6)
|
||||
poolAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
|
||||
fee := uint32(3000)
|
||||
recipient := common.HexToAddress("0x0000000000000000000000000000000000000002")
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = encoder.EncodeSwap(
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amountIn,
|
||||
minAmountOut,
|
||||
poolAddress,
|
||||
fee,
|
||||
recipient,
|
||||
deadline,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUniswapV3Encoder_buildEncodedPath(b *testing.B) {
|
||||
encoder := NewUniswapV3Encoder()
|
||||
|
||||
opp := &arbitrage.Opportunity{
|
||||
Path: []arbitrage.SwapStep{
|
||||
{
|
||||
TokenIn: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
|
||||
TokenOut: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
Fee: 3000,
|
||||
},
|
||||
{
|
||||
TokenIn: common.HexToAddress("0xFF970a61A04b1cA14834A43f5dE4533eBDDB5CC8"),
|
||||
TokenOut: common.HexToAddress("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
|
||||
Fee: 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = encoder.buildEncodedPath(opp)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user