Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
package arbitrage
|
|
|
|
import (
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
|
|
"github.com/fraktal/mev-beta/bindings/contracts"
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
|
|
)
|
|
|
|
type mockArbitrageLogParser struct {
|
|
event *contracts.ArbitrageExecutorArbitrageExecuted
|
|
err error
|
|
}
|
|
|
|
func (m *mockArbitrageLogParser) ParseArbitrageExecuted(types.Log) (*contracts.ArbitrageExecutorArbitrageExecuted, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return m.event, nil
|
|
}
|
|
|
|
func newTestLogger() *logger.Logger {
|
|
return logger.New("error", "text", "")
|
|
}
|
|
|
|
func TestCalculateActualProfit_UsesArbitrageEvent(t *testing.T) {
|
|
arbitrageAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
|
executor := NewFlashSwapExecutor(nil, newTestLogger(), nil, nil, common.Address{}, arbitrageAddr, ExecutionConfig{})
|
|
|
|
profit := new(big.Int).Mul(big.NewInt(2), powerOfTenInt(18))
|
|
gasPrice := big.NewInt(1_000_000_000) // 1 gwei
|
|
receipt := &types.Receipt{
|
|
Logs: []*types.Log{{Address: arbitrageAddr}},
|
|
GasUsed: 100000,
|
|
EffectiveGasPrice: gasPrice,
|
|
}
|
|
|
|
event := &contracts.ArbitrageExecutorArbitrageExecuted{
|
|
Tokens: []common.Address{executor.ethReferenceToken},
|
|
Amounts: []*big.Int{profit},
|
|
Profit: profit,
|
|
}
|
|
|
|
executor.arbitrageBinding = &mockArbitrageLogParser{event: event}
|
|
|
|
opportunity := &pkgtypes.ArbitrageOpportunity{
|
|
TokenOut: executor.ethReferenceToken,
|
|
Quantities: &pkgtypes.OpportunityQuantities{
|
|
NetProfit: pkgtypes.DecimalAmount{Symbol: "WETH", Decimals: 18},
|
|
},
|
|
}
|
|
|
|
actual, err := executor.calculateActualProfit(receipt, opportunity)
|
|
if err != nil {
|
|
t.Fatalf("calculateActualProfit returned error: %v", err)
|
|
}
|
|
|
|
gasCost := new(big.Int).Mul(big.NewInt(0).SetUint64(receipt.GasUsed), gasPrice)
|
|
expected := new(big.Int).Sub(profit, gasCost)
|
|
|
|
if actual.Value.Cmp(expected) != 0 {
|
|
t.Fatalf("expected profit %s, got %s", expected.String(), actual.Value.String())
|
|
}
|
|
|
|
if actual.Decimals != 18 {
|
|
t.Fatalf("expected decimals 18, got %d", actual.Decimals)
|
|
}
|
|
|
|
if actual.Symbol != "WETH" {
|
|
t.Fatalf("expected symbol WETH, got %s", actual.Symbol)
|
|
}
|
|
}
|
|
|
|
func TestCalculateActualProfit_FallbackToOpportunity(t *testing.T) {
|
|
arbitrageAddr := common.HexToAddress("0x1234567890123456789012345678901234567891")
|
|
executor := NewFlashSwapExecutor(nil, newTestLogger(), nil, nil, common.Address{}, arbitrageAddr, ExecutionConfig{})
|
|
|
|
profit := big.NewInt(1_500_000) // 1.5 USDC with 6 decimals
|
|
gasPrice := big.NewInt(1_000_000_000) // 1 gwei
|
|
receipt := &types.Receipt{
|
|
Logs: []*types.Log{{Address: arbitrageAddr}},
|
|
GasUsed: 100000,
|
|
EffectiveGasPrice: gasPrice,
|
|
}
|
|
|
|
opportunity := &pkgtypes.ArbitrageOpportunity{
|
|
TokenOut: common.HexToAddress("0xaF88d065e77c8cC2239327C5EDb3A432268e5831"), // USDC
|
|
NetProfit: profit,
|
|
Quantities: &pkgtypes.OpportunityQuantities{
|
|
NetProfit: pkgtypes.DecimalAmount{Symbol: "USDC", Decimals: 6},
|
|
},
|
|
}
|
|
|
|
actual, err := executor.calculateActualProfit(receipt, opportunity)
|
|
if err != nil {
|
|
t.Fatalf("calculateActualProfit returned error: %v", err)
|
|
}
|
|
|
|
gasCostEth := new(big.Int).Mul(big.NewInt(0).SetUint64(receipt.GasUsed), gasPrice)
|
|
// Gas cost conversion: 0.0001 ETH * 2000 USD / 1 USD = 0.2 USDC => 200000 units
|
|
gasCostUSDC := big.NewInt(200000)
|
|
expected := new(big.Int).Sub(profit, gasCostUSDC)
|
|
|
|
if actual.Value.Cmp(expected) != 0 {
|
|
t.Fatalf("expected profit %s, got %s", expected.String(), actual.Value.String())
|
|
}
|
|
|
|
if actual.Decimals != 6 {
|
|
t.Fatalf("expected decimals 6, got %d", actual.Decimals)
|
|
}
|
|
|
|
if actual.Symbol != "USDC" {
|
|
t.Fatalf("expected symbol USDC, got %s", actual.Symbol)
|
|
}
|
|
|
|
// Ensure ETH gas cost unchanged for reference
|
|
if gasCostEth.Sign() == 0 {
|
|
t.Fatalf("expected non-zero gas cost")
|
|
}
|
|
}
|
|
|
|
func TestCalculateActualProfit_NoPriceData(t *testing.T) {
|
|
arbitrageAddr := common.HexToAddress("0x1234567890123456789012345678901234567892")
|
|
executor := NewFlashSwapExecutor(nil, newTestLogger(), nil, nil, common.Address{}, arbitrageAddr, ExecutionConfig{})
|
|
|
|
profit := new(big.Int).Mul(big.NewInt(3), powerOfTenInt(17)) // 0.3 units with 18 decimals
|
|
receipt := &types.Receipt{
|
|
Logs: []*types.Log{{Address: arbitrageAddr}},
|
|
GasUsed: 50000,
|
|
EffectiveGasPrice: big.NewInt(2_000_000_000),
|
|
}
|
|
|
|
unknownToken := common.HexToAddress("0x9b8D58d870495459c1004C34357F3bf06c0dB0b3")
|
|
opportunity := &pkgtypes.ArbitrageOpportunity{
|
|
TokenOut: unknownToken,
|
|
NetProfit: profit,
|
|
Quantities: &pkgtypes.OpportunityQuantities{
|
|
NetProfit: pkgtypes.DecimalAmount{Symbol: "XYZ", Decimals: 18},
|
|
},
|
|
}
|
|
|
|
actual, err := executor.calculateActualProfit(receipt, opportunity)
|
|
if err != nil {
|
|
t.Fatalf("calculateActualProfit returned error: %v", err)
|
|
}
|
|
|
|
if actual.Value.Cmp(profit) != 0 {
|
|
t.Fatalf("expected profit %s, got %s", profit.String(), actual.Value.String())
|
|
}
|
|
|
|
if actual.Symbol != "XYZ" {
|
|
t.Fatalf("expected symbol XYZ, got %s", actual.Symbol)
|
|
}
|
|
}
|
|
|
|
func TestParseRevertReason_ErrorString(t *testing.T) {
|
|
strType, err := abi.NewType("string", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to create ABI type: %v", err)
|
|
}
|
|
|
|
args := abi.Arguments{{Type: strType}}
|
|
payload, err := args.Pack("execution reverted: slippage limit")
|
|
if err != nil {
|
|
t.Fatalf("failed to pack revert reason: %v", err)
|
|
}
|
|
|
|
data := append([]byte{0x08, 0xc3, 0x79, 0xa0}, payload...)
|
|
reason := parseRevertReason(data)
|
|
if reason != "execution reverted: slippage limit" {
|
|
t.Fatalf("expected revert reason, got %q", reason)
|
|
}
|
|
}
|
|
|
|
func TestParseRevertReason_PanicCode(t *testing.T) {
|
|
uintType, err := abi.NewType("uint256", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to create uint256 ABI type: %v", err)
|
|
}
|
|
|
|
args := abi.Arguments{{Type: uintType}}
|
|
payload, err := args.Pack(big.NewInt(0x41))
|
|
if err != nil {
|
|
t.Fatalf("failed to pack panic code: %v", err)
|
|
}
|
|
|
|
data := append([]byte{0x4e, 0x48, 0x7b, 0x71}, payload...)
|
|
reason := parseRevertReason(data)
|
|
if reason != "panic code 0x41" {
|
|
t.Fatalf("expected panic code, got %q", reason)
|
|
}
|
|
}
|
|
|
|
func TestParseRevertReason_Unknown(t *testing.T) {
|
|
reason := parseRevertReason([]byte{0x00, 0x01, 0x02, 0x03})
|
|
if reason != "" {
|
|
t.Fatalf("expected empty reason, got %q", reason)
|
|
}
|
|
}
|