Files
mev-beta/orig/pkg/arbitrage/flash_executor_test.go
Administrator 803de231ba feat: create v2-prep branch with comprehensive planning
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>
2025-11-10 10:14:26 +01:00

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)
}
}