Files
mev-beta/pkg/parsers/uniswap_v3.go
Administrator d6993a6d98
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
feat(parsers): implement UniswapV3 parser with concentrated liquidity support
**Implementation:**
- Created UniswapV3Parser with ParseLog() and ParseReceipt() methods
- V3 event signature: Swap(address,address,int256,int256,uint160,uint128,int24)
- Signed integer handling (int256) for amounts
- Automatic conversion: negative = input, positive = output
- SqrtPriceX96 decoding (Q64.96 fixed-point format)
- Liquidity and tick tracking from event data
- Token extraction from pool cache with decimal scaling

**Key Differences from V2:**
- Signed amounts (int256) instead of separate in/out fields
- Only 2 amounts (amount0, amount1) vs 4 in V2
- SqrtPriceX96 for price representation
- Liquidity (uint128) tracking
- Tick (int24) tracking for concentrated liquidity positions
- sender and recipient both indexed (in topics)

**Testing:**
- Comprehensive unit tests with 100% coverage
- Tests for both positive and negative amounts
- Edge cases: both negative, both positive (invalid but parsed)
- Decimal scaling validation (18 decimals and 6 decimals)
- Two's complement encoding for negative numbers
- Tick handling (positive and negative)
- Mixed V2/V3 event filtering in receipts

**Price Calculation:**
- CalculatePriceFromSqrtPriceX96() helper function
- Converts Q64.96 format to human-readable price
- Price = (sqrtPriceX96 / 2^96)^2
- Adjusts for decimal differences between tokens

**Type System:**
- Exported ScaleToDecimals() for cross-parser usage
- Updated existing tests to use exported function
- Consistent decimal handling across V2 and V3 parsers

**Use Cases:**
1. Parse V3 swaps: parser.ParseLog() with signed amount conversion
2. Track price movements: CalculatePriceFromSqrtPriceX96()
3. Monitor liquidity changes: event.Liquidity
4. Track tick positions: event.Tick
5. Multi-hop arbitrage: ParseReceipt() for complex routes

**Task:** P2-010 (UniswapV3 parser base implementation)
**Coverage:** 100% (enforced in CI/CD)
**Protocol:** UniswapV3 on Arbitrum

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 15:37:01 +01:00

254 lines
7.7 KiB
Go

package parsers
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/your-org/mev-bot/pkg/cache"
mevtypes "github.com/your-org/mev-bot/pkg/types"
)
// UniswapV3 Swap event signature:
// event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
var (
// SwapV3EventSignature is the event signature for UniswapV3 Swap events
SwapV3EventSignature = crypto.Keccak256Hash([]byte("Swap(address,address,int256,int256,uint160,uint128,int24)"))
)
// UniswapV3Parser implements the Parser interface for UniswapV3 pools
type UniswapV3Parser struct {
cache cache.PoolCache
logger mevtypes.Logger
}
// NewUniswapV3Parser creates a new UniswapV3 parser
func NewUniswapV3Parser(cache cache.PoolCache, logger mevtypes.Logger) *UniswapV3Parser {
return &UniswapV3Parser{
cache: cache,
logger: logger,
}
}
// Protocol returns the protocol type this parser handles
func (p *UniswapV3Parser) Protocol() mevtypes.ProtocolType {
return mevtypes.ProtocolUniswapV3
}
// SupportsLog checks if this parser can handle the given log
func (p *UniswapV3Parser) SupportsLog(log types.Log) bool {
// Check if log has the Swap event signature
if len(log.Topics) == 0 {
return false
}
return log.Topics[0] == SwapV3EventSignature
}
// ParseLog parses a UniswapV3 Swap event from a log
func (p *UniswapV3Parser) ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error) {
// Verify this is a Swap event
if !p.SupportsLog(log) {
return nil, fmt.Errorf("unsupported log")
}
// Get pool info from cache to extract token addresses and decimals
poolInfo, err := p.cache.GetByAddress(ctx, log.Address)
if err != nil {
return nil, fmt.Errorf("pool not found in cache: %w", err)
}
// Parse event data
// Data contains: amount0, amount1, sqrtPriceX96, liquidity, tick (non-indexed)
// Topics contain: [signature, sender, recipient] (indexed)
if len(log.Topics) != 3 {
return nil, fmt.Errorf("invalid number of topics: expected 3, got %d", len(log.Topics))
}
// Define ABI for data decoding
int256Type, err := abi.NewType("int256", "", nil)
if err != nil {
return nil, fmt.Errorf("failed to create int256 type: %w", err)
}
uint160Type, err := abi.NewType("uint160", "", nil)
if err != nil {
return nil, fmt.Errorf("failed to create uint160 type: %w", err)
}
uint128Type, err := abi.NewType("uint128", "", nil)
if err != nil {
return nil, fmt.Errorf("failed to create uint128 type: %w", err)
}
int24Type, err := abi.NewType("int24", "", nil)
if err != nil {
return nil, fmt.Errorf("failed to create int24 type: %w", err)
}
arguments := abi.Arguments{
{Type: int256Type, Name: "amount0"},
{Type: int256Type, Name: "amount1"},
{Type: uint160Type, Name: "sqrtPriceX96"},
{Type: uint128Type, Name: "liquidity"},
{Type: int24Type, Name: "tick"},
}
// Decode data
values, err := arguments.Unpack(log.Data)
if err != nil {
return nil, fmt.Errorf("failed to decode event data: %w", err)
}
if len(values) != 5 {
return nil, fmt.Errorf("invalid number of values: expected 5, got %d", len(values))
}
// Extract indexed parameters from topics
sender := common.BytesToAddress(log.Topics[1].Bytes())
recipient := common.BytesToAddress(log.Topics[2].Bytes())
// Extract amounts from decoded data (signed integers)
amount0Signed := values[0].(*big.Int)
amount1Signed := values[1].(*big.Int)
sqrtPriceX96 := values[2].(*big.Int)
liquidity := values[3].(*big.Int)
tick := values[4].(*big.Int) // int24 is returned as *big.Int
// Convert signed amounts to in/out amounts
// Positive amount = token added to pool (user receives this token = out)
// Negative amount = token removed from pool (user sends this token = in)
var amount0In, amount0Out, amount1In, amount1Out *big.Int
if amount0Signed.Sign() < 0 {
// Negative = input (user sends token0)
amount0In = new(big.Int).Abs(amount0Signed)
amount0Out = big.NewInt(0)
} else {
// Positive = output (user receives token0)
amount0In = big.NewInt(0)
amount0Out = new(big.Int).Set(amount0Signed)
}
if amount1Signed.Sign() < 0 {
// Negative = input (user sends token1)
amount1In = new(big.Int).Abs(amount1Signed)
amount1Out = big.NewInt(0)
} else {
// Positive = output (user receives token1)
amount1In = big.NewInt(0)
amount1Out = new(big.Int).Set(amount1Signed)
}
// Scale amounts to 18 decimals for internal representation
amount0InScaled := mevtypes.ScaleToDecimals(amount0In, poolInfo.Token0Decimals, 18)
amount1InScaled := mevtypes.ScaleToDecimals(amount1In, poolInfo.Token1Decimals, 18)
amount0OutScaled := mevtypes.ScaleToDecimals(amount0Out, poolInfo.Token0Decimals, 18)
amount1OutScaled := mevtypes.ScaleToDecimals(amount1Out, poolInfo.Token1Decimals, 18)
// Convert tick from *big.Int to *int32
tickInt64 := tick.Int64()
tickInt32 := int32(tickInt64)
// Create swap event
event := &mevtypes.SwapEvent{
TxHash: tx.Hash(),
BlockNumber: log.BlockNumber,
LogIndex: uint(log.Index),
PoolAddress: log.Address,
Protocol: mevtypes.ProtocolUniswapV3,
Token0: poolInfo.Token0,
Token1: poolInfo.Token1,
Token0Decimals: poolInfo.Token0Decimals,
Token1Decimals: poolInfo.Token1Decimals,
Amount0In: amount0InScaled,
Amount1In: amount1InScaled,
Amount0Out: amount0OutScaled,
Amount1Out: amount1OutScaled,
Sender: sender,
Recipient: recipient,
Fee: big.NewInt(int64(poolInfo.Fee)),
SqrtPriceX96: sqrtPriceX96,
Liquidity: liquidity,
Tick: &tickInt32,
}
// Validate the parsed event
if err := event.Validate(); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
p.logger.Debug("parsed UniswapV3 swap event",
"txHash", event.TxHash.Hex(),
"pool", event.PoolAddress.Hex(),
"token0", event.Token0.Hex(),
"token1", event.Token1.Hex(),
"tick", tickInt32,
"sqrtPriceX96", sqrtPriceX96.String(),
)
return event, nil
}
// ParseReceipt parses all UniswapV3 Swap events from a transaction receipt
func (p *UniswapV3Parser) ParseReceipt(ctx context.Context, receipt *types.Receipt, tx *types.Transaction) ([]*mevtypes.SwapEvent, error) {
var events []*mevtypes.SwapEvent
for _, log := range receipt.Logs {
if p.SupportsLog(*log) {
event, err := p.ParseLog(ctx, *log, tx)
if err != nil {
// Log error but continue processing other logs
p.logger.Warn("failed to parse log",
"txHash", tx.Hash().Hex(),
"logIndex", log.Index,
"error", err,
)
continue
}
events = append(events, event)
}
}
return events, nil
}
// CalculatePriceFromSqrtPriceX96 converts sqrtPriceX96 to a human-readable price
// Price = (sqrtPriceX96 / 2^96)^2
func CalculatePriceFromSqrtPriceX96(sqrtPriceX96 *big.Int, token0Decimals, token1Decimals uint8) *big.Float {
if sqrtPriceX96 == nil || sqrtPriceX96.Sign() == 0 {
return big.NewFloat(0)
}
// sqrtPriceX96 is Q64.96 format (fixed-point with 96 fractional bits)
// Price = (sqrtPriceX96 / 2^96)^2
// Convert to float
sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96)
// Divide by 2^96
divisor := new(big.Float).SetInt(new(big.Int).Lsh(big.NewInt(1), 96))
sqrtPrice := new(big.Float).Quo(sqrtPriceFloat, divisor)
// Square to get price
price := new(big.Float).Mul(sqrtPrice, sqrtPrice)
// Adjust for decimal differences
if token0Decimals != token1Decimals {
decimalAdjustment := new(big.Float).SetInt(
new(big.Int).Exp(
big.NewInt(10),
big.NewInt(int64(token0Decimals)-int64(token1Decimals)),
nil,
),
)
price = new(big.Float).Mul(price, decimalAdjustment)
}
return price
}