Compare commits
4 Commits
v2-master
...
feature/v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9166c3f707 | ||
|
|
d6993a6d98 | ||
|
|
af2e9e9a1f | ||
|
|
114bc6dd79 |
450
docs/BRANCH_STRUCTURE.md
Normal file
450
docs/BRANCH_STRUCTURE.md
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
# V2 Branch Structure
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The MEV Bot V2 project uses a structured branching strategy to maintain code quality, enable parallel development, and ensure production stability.
|
||||||
|
|
||||||
|
## Branch Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
v2-master (production)
|
||||||
|
↑
|
||||||
|
└── v2-master-dev (development)
|
||||||
|
↑
|
||||||
|
├── feature/v2/parsers/*
|
||||||
|
├── feature/v2/arbitrage/*
|
||||||
|
├── feature/v2/execution/*
|
||||||
|
└── feature/v2/*
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch Descriptions
|
||||||
|
|
||||||
|
### 🔒 `v2-master` (Production Branch)
|
||||||
|
|
||||||
|
**Purpose:** Production-ready code only
|
||||||
|
**Protection:** Protected, requires PR approval
|
||||||
|
**Updates:** Only from `v2-master-dev` via merge
|
||||||
|
**CI/CD:** Full pipeline on every push
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- ✅ Foundation complete (100% coverage)
|
||||||
|
- ✅ CI/CD configured
|
||||||
|
- ✅ Documentation complete
|
||||||
|
- ✅ Ready for production deployment
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- ❌ NEVER commit directly to v2-master
|
||||||
|
- ✅ Only merge from v2-master-dev
|
||||||
|
- ✅ Must pass all CI/CD checks
|
||||||
|
- ✅ Requires code review approval
|
||||||
|
- ✅ Must maintain 100% test coverage
|
||||||
|
|
||||||
|
**When to merge:**
|
||||||
|
- After thorough testing in v2-master-dev
|
||||||
|
- When ready for production deployment
|
||||||
|
- After all features in a release are complete
|
||||||
|
- When stability is confirmed
|
||||||
|
|
||||||
|
### 🔧 `v2-master-dev` (Development Branch)
|
||||||
|
|
||||||
|
**Purpose:** Integration and testing of new features
|
||||||
|
**Protection:** Protected, requires PR approval
|
||||||
|
**Updates:** From feature branches via PR
|
||||||
|
**CI/CD:** Full pipeline on every push
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- ✅ Foundation complete (100% coverage)
|
||||||
|
- ✅ All infrastructure ready
|
||||||
|
- ⏳ Ready for protocol parser development
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- ❌ NEVER commit directly to v2-master-dev
|
||||||
|
- ✅ Only merge from feature/v2/* branches
|
||||||
|
- ✅ Must pass all CI/CD checks (100% coverage enforced)
|
||||||
|
- ✅ Requires code review
|
||||||
|
- ✅ Acts as staging for v2-master
|
||||||
|
|
||||||
|
**When to merge:**
|
||||||
|
- Feature is complete with 100% test coverage
|
||||||
|
- All CI/CD checks pass
|
||||||
|
- Code review approved
|
||||||
|
- Integration tests pass
|
||||||
|
|
||||||
|
### 🌿 `feature/v2/*` (Feature Branches)
|
||||||
|
|
||||||
|
**Purpose:** Development of individual features/tasks
|
||||||
|
**Protection:** None (local development)
|
||||||
|
**Updates:** Created from v2-master-dev
|
||||||
|
**CI/CD:** Full pipeline on push
|
||||||
|
|
||||||
|
**Naming Convention:**
|
||||||
|
```
|
||||||
|
feature/v2/<component>/<task-id>-<description>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
feature/v2/parsers/P2-002-uniswap-v2-base
|
||||||
|
feature/v2/parsers/P2-010-uniswap-v3-base
|
||||||
|
feature/v2/arbitrage/P5-001-path-finder
|
||||||
|
feature/v2/execution/P6-001-front-runner
|
||||||
|
feature/v2/cache/P3-006-liquidity-index
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- ✅ ALWAYS create from v2-master-dev
|
||||||
|
- ✅ Branch name MUST match task ID from planning docs
|
||||||
|
- ✅ One feature per branch
|
||||||
|
- ✅ Must achieve 100% test coverage
|
||||||
|
- ✅ Delete branch after merge
|
||||||
|
- ✅ Keep branches small and focused (< 2 hours work)
|
||||||
|
|
||||||
|
### 📦 `feature/v2-prep` (Foundation Branch - Archived)
|
||||||
|
|
||||||
|
**Purpose:** V2 planning and foundation implementation
|
||||||
|
**Status:** ✅ Complete and archived
|
||||||
|
**Protection:** Read-only
|
||||||
|
|
||||||
|
**What was implemented:**
|
||||||
|
- Complete planning documentation (7 docs)
|
||||||
|
- Core types and interfaces
|
||||||
|
- Parser factory
|
||||||
|
- Multi-index cache
|
||||||
|
- Validation pipeline
|
||||||
|
- Observability infrastructure
|
||||||
|
- CI/CD pipeline
|
||||||
|
- Git hooks
|
||||||
|
|
||||||
|
**Note:** This branch is now archived. All new development should branch from `v2-master-dev`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Examples
|
||||||
|
|
||||||
|
### 🎯 Standard Feature Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Start from v2-master-dev
|
||||||
|
git checkout v2-master-dev
|
||||||
|
git pull origin v2-master-dev
|
||||||
|
|
||||||
|
# 2. Create feature branch
|
||||||
|
git checkout -b feature/v2/parsers/P2-002-uniswap-v2-base
|
||||||
|
|
||||||
|
# 3. Implement feature with TDD
|
||||||
|
# Write tests first, then implementation
|
||||||
|
make test-coverage # Must show 100%
|
||||||
|
|
||||||
|
# 4. Validate locally
|
||||||
|
make validate # Runs all CI/CD checks
|
||||||
|
|
||||||
|
# 5. Commit with conventional format
|
||||||
|
git add .
|
||||||
|
git commit -m "feat(parsers): implement UniswapV2 parser base
|
||||||
|
|
||||||
|
- Created parser struct with dependencies
|
||||||
|
- Implemented ParseLog() for Swap events
|
||||||
|
- Added comprehensive test suite
|
||||||
|
- Achieved 100% test coverage
|
||||||
|
|
||||||
|
Task: P2-002
|
||||||
|
Coverage: 100%
|
||||||
|
Tests: 42/42 passing
|
||||||
|
|
||||||
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||||
|
|
||||||
|
# 6. Push and create PR
|
||||||
|
git push -u origin feature/v2/parsers/P2-002-uniswap-v2-base
|
||||||
|
|
||||||
|
# 7. Create PR on GitHub targeting v2-master-dev
|
||||||
|
# Wait for CI/CD to pass
|
||||||
|
# Get code review approval
|
||||||
|
|
||||||
|
# 8. Merge PR (squash and merge)
|
||||||
|
# Delete feature branch on GitHub
|
||||||
|
|
||||||
|
# 9. Cleanup local branch
|
||||||
|
git checkout v2-master-dev
|
||||||
|
git pull origin v2-master-dev
|
||||||
|
git branch -d feature/v2/parsers/P2-002-uniswap-v2-base
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 Production Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# When v2-master-dev is stable and tested
|
||||||
|
git checkout v2-master
|
||||||
|
git pull origin v2-master
|
||||||
|
|
||||||
|
# Merge development into production
|
||||||
|
git merge --no-ff v2-master-dev -m "Release: Protocol parsers v1.0
|
||||||
|
|
||||||
|
Includes:
|
||||||
|
- UniswapV2 parser (100% coverage)
|
||||||
|
- UniswapV3 parser (100% coverage)
|
||||||
|
- Curve parser (100% coverage)
|
||||||
|
- Integration tests (all passing)
|
||||||
|
|
||||||
|
Total coverage: 100%
|
||||||
|
CI/CD: All checks passing"
|
||||||
|
|
||||||
|
# Push to production
|
||||||
|
git push origin v2-master
|
||||||
|
|
||||||
|
# Sync v2-master-dev
|
||||||
|
git checkout v2-master-dev
|
||||||
|
git merge v2-master
|
||||||
|
git push origin v2-master-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Hotfix for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create hotfix branch from v2-master
|
||||||
|
git checkout v2-master
|
||||||
|
git pull origin v2-master
|
||||||
|
git checkout -b hotfix/fix-decimal-precision
|
||||||
|
|
||||||
|
# Fix the issue with tests
|
||||||
|
# ... implement fix ...
|
||||||
|
make test-coverage # Must show 100%
|
||||||
|
make validate
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
git commit -m "fix(types): correct decimal scaling for USDC
|
||||||
|
|
||||||
|
- Fixed scaleToDecimals() rounding issue
|
||||||
|
- Added test case for 6-decimal tokens
|
||||||
|
- Verified against production data
|
||||||
|
|
||||||
|
Coverage: 100%
|
||||||
|
Tests: 156/156 passing"
|
||||||
|
|
||||||
|
# Merge to both v2-master and v2-master-dev
|
||||||
|
git checkout v2-master
|
||||||
|
git merge --no-ff hotfix/fix-decimal-precision
|
||||||
|
git push origin v2-master
|
||||||
|
|
||||||
|
git checkout v2-master-dev
|
||||||
|
git merge hotfix/fix-decimal-precision
|
||||||
|
git push origin v2-master-dev
|
||||||
|
|
||||||
|
# Delete hotfix branch
|
||||||
|
git branch -d hotfix/fix-decimal-precision
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch Protection Rules
|
||||||
|
|
||||||
|
### v2-master
|
||||||
|
|
||||||
|
- ✅ Require pull request before merging
|
||||||
|
- ✅ Require 1 approval
|
||||||
|
- ✅ Require status checks to pass:
|
||||||
|
- Pre-flight checks
|
||||||
|
- Build & dependencies
|
||||||
|
- Code quality (40+ linters)
|
||||||
|
- **100% test coverage (enforced)**
|
||||||
|
- Integration tests
|
||||||
|
- Modularity validation
|
||||||
|
- ✅ Require conversation resolution
|
||||||
|
- ✅ Require linear history
|
||||||
|
- ❌ Do not allow bypassing
|
||||||
|
|
||||||
|
### v2-master-dev
|
||||||
|
|
||||||
|
- ✅ Require pull request before merging
|
||||||
|
- ✅ Require 1 approval
|
||||||
|
- ✅ Require status checks to pass:
|
||||||
|
- All CI/CD checks
|
||||||
|
- **100% test coverage (enforced)**
|
||||||
|
- ✅ Require conversation resolution
|
||||||
|
- ❌ Do not allow bypassing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD Pipeline
|
||||||
|
|
||||||
|
### Triggers
|
||||||
|
|
||||||
|
**On Push to:**
|
||||||
|
- `v2-master`
|
||||||
|
- `v2-master-dev`
|
||||||
|
- `feature/v2/**`
|
||||||
|
|
||||||
|
**On Pull Request to:**
|
||||||
|
- `v2-master`
|
||||||
|
- `v2-master-dev`
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
All branches must pass:
|
||||||
|
|
||||||
|
1. **Pre-flight**
|
||||||
|
- Branch naming validation
|
||||||
|
- Commit message format
|
||||||
|
|
||||||
|
2. **Build & Dependencies**
|
||||||
|
- Go compilation
|
||||||
|
- Dependency verification
|
||||||
|
- go.mod tidiness
|
||||||
|
|
||||||
|
3. **Code Quality**
|
||||||
|
- gofmt formatting
|
||||||
|
- go vet static analysis
|
||||||
|
- golangci-lint (40+ linters)
|
||||||
|
- gosec security scanning
|
||||||
|
|
||||||
|
4. **Tests**
|
||||||
|
- Unit tests with race detector
|
||||||
|
- **100% coverage enforcement** ⚠️
|
||||||
|
- Integration tests
|
||||||
|
- Decimal precision tests
|
||||||
|
|
||||||
|
5. **Modularity**
|
||||||
|
- Component independence
|
||||||
|
- No circular dependencies
|
||||||
|
|
||||||
|
6. **Performance**
|
||||||
|
- Benchmarks (when requested)
|
||||||
|
|
||||||
|
### Coverage Enforcement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CI/CD fails if coverage < 100%
|
||||||
|
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||||
|
|
||||||
|
if [ $(echo "$COVERAGE < 100" | bc -l) -eq 1 ]; then
|
||||||
|
echo "❌ COVERAGE FAILURE: $COVERAGE% < 100%"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**This is non-negotiable.** All code must have 100% test coverage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
### ✅ Complete
|
||||||
|
|
||||||
|
**v2-master** (Production)
|
||||||
|
- Foundation: 100% complete
|
||||||
|
- Test Coverage: 100% (enforced)
|
||||||
|
- Documentation: Complete
|
||||||
|
- CI/CD: Configured and tested
|
||||||
|
|
||||||
|
**v2-master-dev** (Development)
|
||||||
|
- Foundation: 100% complete
|
||||||
|
- Test Coverage: 100% (enforced)
|
||||||
|
- Ready for: Protocol parser development
|
||||||
|
|
||||||
|
**feature/v2-prep** (Archived)
|
||||||
|
- Planning: 7 comprehensive documents
|
||||||
|
- Foundation: Complete implementation
|
||||||
|
- Status: Archived, read-only
|
||||||
|
|
||||||
|
### ⏳ In Progress
|
||||||
|
|
||||||
|
**Phase 2:** Protocol Parser Implementations
|
||||||
|
- UniswapV2 parser
|
||||||
|
- UniswapV3 parser
|
||||||
|
- Curve parser
|
||||||
|
- Balancer V2 parser
|
||||||
|
- Kyber parsers
|
||||||
|
- Camelot parsers
|
||||||
|
|
||||||
|
### 📋 Planned
|
||||||
|
|
||||||
|
**Phase 3:** Arbitrage Detection
|
||||||
|
**Phase 4:** Execution Engine
|
||||||
|
**Phase 5:** Sequencer Integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✅ DO
|
||||||
|
|
||||||
|
- Create feature branches from `v2-master-dev`
|
||||||
|
- Follow the naming convention strictly
|
||||||
|
- Write tests before implementation (TDD)
|
||||||
|
- Run `make validate` before pushing
|
||||||
|
- Keep commits small and focused
|
||||||
|
- Use conventional commit messages
|
||||||
|
- Delete branches after merge
|
||||||
|
- Review planning docs before implementation
|
||||||
|
|
||||||
|
### ❌ DON'T
|
||||||
|
|
||||||
|
- Never commit directly to protected branches
|
||||||
|
- Never bypass CI/CD checks
|
||||||
|
- Never merge without 100% coverage
|
||||||
|
- Never skip code review
|
||||||
|
- Don't create long-lived feature branches
|
||||||
|
- Don't implement without tests
|
||||||
|
- Don't merge failing builds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create feature branch
|
||||||
|
git checkout v2-master-dev
|
||||||
|
git pull
|
||||||
|
git checkout -b feature/v2/<component>/<task-id>-<description>
|
||||||
|
|
||||||
|
# Validate locally
|
||||||
|
make validate
|
||||||
|
|
||||||
|
# Test with coverage
|
||||||
|
make test-coverage
|
||||||
|
|
||||||
|
# Create PR (via GitHub UI or gh CLI)
|
||||||
|
gh pr create --base v2-master-dev --title "feat: description"
|
||||||
|
|
||||||
|
# Merge to production
|
||||||
|
git checkout v2-master
|
||||||
|
git merge --no-ff v2-master-dev
|
||||||
|
git push origin v2-master
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branch Overview
|
||||||
|
|
||||||
|
| Branch | Purpose | Source | Protection | Coverage |
|
||||||
|
|--------|---------|--------|------------|----------|
|
||||||
|
| `v2-master` | Production | `v2-master-dev` | Protected | 100% |
|
||||||
|
| `v2-master-dev` | Development | `feature/v2/*` | Protected | 100% |
|
||||||
|
| `feature/v2/*` | Features | `v2-master-dev` | None | 100% |
|
||||||
|
| `feature/v2-prep` | Foundation | - | Archived | 100% |
|
||||||
|
|
||||||
|
### Coverage Requirements
|
||||||
|
|
||||||
|
All branches: **100% test coverage (enforced by CI/CD)**
|
||||||
|
|
||||||
|
No exceptions. No workarounds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Planning:** `docs/planning/`
|
||||||
|
- **Status:** `docs/V2_IMPLEMENTATION_STATUS.md`
|
||||||
|
- **Guidance:** `CLAUDE.md`
|
||||||
|
- **Overview:** `README.md`
|
||||||
|
- **CI/CD:** `.github/workflows/v2-ci.yml`
|
||||||
|
- **Hooks:** `.git-hooks/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-11-10
|
||||||
|
**Status:** v2-master and v2-master-dev created and synced
|
||||||
|
**Foundation:** ✅ Complete with 100% coverage
|
||||||
|
**Next:** Protocol parser implementations
|
||||||
440
pkg/parsers/UNISWAP_V3_MATH.md
Normal file
440
pkg/parsers/UNISWAP_V3_MATH.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# Uniswap V3 Math Utilities
|
||||||
|
|
||||||
|
Comprehensive mathematical utilities for Uniswap V3 concentrated liquidity pools. Based on the official Uniswap V3 SDK and whitepaper.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Uniswap V3 uses concentrated liquidity with tick-based price ranges. All prices are represented as `sqrtPriceX96` (Q64.96 fixed-point format), and positions are defined by tick ranges.
|
||||||
|
|
||||||
|
### Key Concepts
|
||||||
|
|
||||||
|
**1. Ticks**
|
||||||
|
- Discrete price levels: `price = 1.0001^tick`
|
||||||
|
- Valid range: `-887272` to `887272`
|
||||||
|
- Each tick represents a 0.01% price change
|
||||||
|
|
||||||
|
**2. SqrtPriceX96**
|
||||||
|
- Fixed-point representation: `sqrtPriceX96 = sqrt(price) * 2^96`
|
||||||
|
- Q64.96 format (64 integer bits, 96 fractional bits)
|
||||||
|
- Used internally for all price calculations
|
||||||
|
|
||||||
|
**3. Liquidity**
|
||||||
|
- Virtual liquidity representing swap capacity
|
||||||
|
- Changes at tick boundaries
|
||||||
|
- Determines slippage for swaps
|
||||||
|
|
||||||
|
## Core Functions
|
||||||
|
|
||||||
|
### Tick ↔ Price Conversion
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Convert tick to sqrtPriceX96
|
||||||
|
sqrtPrice, err := GetSqrtRatioAtTick(tick)
|
||||||
|
|
||||||
|
// Convert sqrtPriceX96 to tick
|
||||||
|
tick, err := GetTickAtSqrtRatio(sqrtPriceX96)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```go
|
||||||
|
// Get price at tick 0 (price = 1)
|
||||||
|
tick := int32(0)
|
||||||
|
sqrtPrice, _ := GetSqrtRatioAtTick(tick)
|
||||||
|
// sqrtPrice ≈ 2^96 = 79228162514264337593543950336
|
||||||
|
|
||||||
|
// Convert back
|
||||||
|
calculatedTick, _ := GetTickAtSqrtRatio(sqrtPrice)
|
||||||
|
// calculatedTick = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Amount Deltas (Liquidity Changes)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Calculate token0 amount for a liquidity change
|
||||||
|
amount0 := GetAmount0Delta(
|
||||||
|
sqrtRatioA, // Lower sqrt price
|
||||||
|
sqrtRatioB, // Upper sqrt price
|
||||||
|
liquidity, // Liquidity amount
|
||||||
|
roundUp, // Round up for safety
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate token1 amount for a liquidity change
|
||||||
|
amount1 := GetAmount1Delta(
|
||||||
|
sqrtRatioA,
|
||||||
|
sqrtRatioB,
|
||||||
|
liquidity,
|
||||||
|
roundUp,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Formulas:**
|
||||||
|
- `amount0 = liquidity * (sqrtB - sqrtA) / (sqrtA * sqrtB)`
|
||||||
|
- `amount1 = liquidity * (sqrtB - sqrtA) / 2^96`
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Calculate how much of each token is needed to add liquidity
|
||||||
|
- Calculate how much of each token received when removing liquidity
|
||||||
|
- Validate swap amounts against expected values
|
||||||
|
|
||||||
|
### Swap Calculations
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Calculate output for exact input swap
|
||||||
|
amountOut, priceAfter, err := CalculateSwapAmounts(
|
||||||
|
sqrtPriceX96, // Current price
|
||||||
|
liquidity, // Pool liquidity
|
||||||
|
amountIn, // Input amount
|
||||||
|
zeroForOne, // true = swap token0→token1, false = token1→token0
|
||||||
|
feePips, // Fee in pips (3000 = 0.3%)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```go
|
||||||
|
// Swap 1 ETH for USDC in 0.3% fee pool
|
||||||
|
currentPrice := pool.SqrtPriceX96
|
||||||
|
liquidity := pool.Liquidity
|
||||||
|
amountIn := big.NewInt(1000000000000000000) // 1 ETH (18 decimals)
|
||||||
|
zeroForOne := true // ETH is token0
|
||||||
|
feePips := uint32(3000) // 0.3%
|
||||||
|
|
||||||
|
usdcOut, newPrice, err := CalculateSwapAmounts(
|
||||||
|
currentPrice,
|
||||||
|
liquidity,
|
||||||
|
amountIn,
|
||||||
|
zeroForOne,
|
||||||
|
feePips,
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Printf("1 ETH → %v USDC\n", usdcOut)
|
||||||
|
fmt.Printf("Price moved from %v to %v\n", currentPrice, newPrice)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Step Swaps (Tick Crossing)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Compute a single swap step within one tick range
|
||||||
|
sqrtPriceNext, amountIn, amountOut, feeAmount, err := ComputeSwapStep(
|
||||||
|
sqrtRatioCurrentX96, // Current price
|
||||||
|
sqrtRatioTargetX96, // Target price (next tick or price limit)
|
||||||
|
liquidity, // Liquidity in this range
|
||||||
|
amountRemaining, // Remaining amount to swap
|
||||||
|
feePips, // Fee in pips
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use Case:** Complex swaps that cross multiple ticks
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```go
|
||||||
|
// Simulate a swap that might cross ticks
|
||||||
|
currentPrice := pool.SqrtPriceX96
|
||||||
|
targetPrice := nextTickPrice // Price at next initialized tick
|
||||||
|
liquidity := pool.Liquidity
|
||||||
|
amountRemaining := big.NewInt(5000000000000000000) // 5 ETH
|
||||||
|
feePips := uint32(3000)
|
||||||
|
|
||||||
|
priceNext, amountIn, amountOut, fee, _ := ComputeSwapStep(
|
||||||
|
currentPrice,
|
||||||
|
targetPrice,
|
||||||
|
liquidity,
|
||||||
|
amountRemaining,
|
||||||
|
feePips,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if we reached the target price
|
||||||
|
if priceNext.Cmp(targetPrice) == 0 {
|
||||||
|
fmt.Println("Reached tick boundary, need to continue swap in next tick")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Swap completed within this tick range")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Arbitrage Detection
|
||||||
|
|
||||||
|
### Simple Two-Pool Arbitrage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Pool 1: WETH/USDC (V3, 0.3%)
|
||||||
|
pool1SqrtPrice := pool1.SqrtPriceX96
|
||||||
|
pool1Liquidity := pool1.Liquidity
|
||||||
|
pool1FeePips := uint32(3000)
|
||||||
|
|
||||||
|
// Pool 2: WETH/USDC (V2)
|
||||||
|
pool2Reserve0 := pool2.Reserve0 // WETH
|
||||||
|
pool2Reserve1 := pool2.Reserve1 // USDC
|
||||||
|
pool2Fee := uint32(30) // 0.3%
|
||||||
|
|
||||||
|
// Calculate output from Pool 1 (V3)
|
||||||
|
amountIn := big.NewInt(1000000000000000000) // 1 WETH
|
||||||
|
usdc1, price1After, _ := CalculateSwapAmounts(
|
||||||
|
pool1SqrtPrice,
|
||||||
|
pool1Liquidity,
|
||||||
|
amountIn,
|
||||||
|
true, // WETH → USDC
|
||||||
|
pool1FeePips,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate output from Pool 2 (V2) using constant product formula
|
||||||
|
// amountOut = (amountIn * 997 * reserve1) / (reserve0 * 1000 + amountIn * 997)
|
||||||
|
numerator := new(big.Int).Mul(amountIn, big.NewInt(997))
|
||||||
|
numerator.Mul(numerator, pool2Reserve1)
|
||||||
|
denominator := new(big.Int).Mul(pool2Reserve0, big.NewInt(1000))
|
||||||
|
amountInWithFee := new(big.Int).Mul(amountIn, big.NewInt(997))
|
||||||
|
denominator.Add(denominator, amountInWithFee)
|
||||||
|
usdc2 := new(big.Int).Div(numerator, denominator)
|
||||||
|
|
||||||
|
// Compare outputs
|
||||||
|
if usdc1.Cmp(usdc2) > 0 {
|
||||||
|
profit := new(big.Int).Sub(usdc1, usdc2)
|
||||||
|
fmt.Printf("Arbitrage opportunity: %v USDC profit\n", profit)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Hop V3 Arbitrage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Route: WETH → USDC → DAI → WETH
|
||||||
|
|
||||||
|
// Step 1: WETH → USDC (V3 0.3%)
|
||||||
|
usdc, priceAfter1, _ := CalculateSwapAmounts(
|
||||||
|
poolWETH_USDC.SqrtPriceX96,
|
||||||
|
poolWETH_USDC.Liquidity,
|
||||||
|
wethInput,
|
||||||
|
true,
|
||||||
|
3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 2: USDC → DAI (V3 0.05%)
|
||||||
|
dai, priceAfter2, _ := CalculateSwapAmounts(
|
||||||
|
poolUSDC_DAI.SqrtPriceX96,
|
||||||
|
poolUSDC_DAI.Liquidity,
|
||||||
|
usdc,
|
||||||
|
true,
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 3: DAI → WETH (V3 0.3%)
|
||||||
|
wethOutput, priceAfter3, _ := CalculateSwapAmounts(
|
||||||
|
poolDAI_WETH.SqrtPriceX96,
|
||||||
|
poolDAI_WETH.Liquidity,
|
||||||
|
dai,
|
||||||
|
false, // DAI → WETH
|
||||||
|
3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate profit
|
||||||
|
profit := new(big.Int).Sub(wethOutput, wethInput)
|
||||||
|
if profit.Sign() > 0 {
|
||||||
|
fmt.Printf("Multi-hop arbitrage profit: %v WETH\n", profit)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sandwich Attack Detection
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Victim's pending transaction
|
||||||
|
victimAmountIn := big.NewInt(10000000000000000000) // 10 ETH
|
||||||
|
victimZeroForOne := true
|
||||||
|
|
||||||
|
// Calculate victim's expected output
|
||||||
|
victimOut, victimPriceAfter, _ := CalculateSwapAmounts(
|
||||||
|
currentPrice,
|
||||||
|
currentLiquidity,
|
||||||
|
victimAmountIn,
|
||||||
|
victimZeroForOne,
|
||||||
|
3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Front-run: Move price against victim
|
||||||
|
frontrunAmountIn := big.NewInt(5000000000000000000) // 5 ETH
|
||||||
|
_, priceAfterFrontrun, _ := CalculateSwapAmounts(
|
||||||
|
currentPrice,
|
||||||
|
currentLiquidity,
|
||||||
|
frontrunAmountIn,
|
||||||
|
victimZeroForOne,
|
||||||
|
3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Victim executes at worse price
|
||||||
|
victimOutActual, priceAfterVictim, _ := CalculateSwapAmounts(
|
||||||
|
priceAfterFrontrun,
|
||||||
|
currentLiquidity,
|
||||||
|
victimAmountIn,
|
||||||
|
victimZeroForOne,
|
||||||
|
3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Back-run: Reverse front-run trade
|
||||||
|
backrunAmountIn := victimOutActual // All the USDC we got
|
||||||
|
backrunOut, finalPrice, _ := CalculateSwapAmounts(
|
||||||
|
priceAfterVictim,
|
||||||
|
currentLiquidity,
|
||||||
|
backrunAmountIn,
|
||||||
|
!victimZeroForOne, // Reverse direction
|
||||||
|
3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate sandwich profit
|
||||||
|
initialCapital := frontrunAmountIn
|
||||||
|
finalCapital := backrunOut
|
||||||
|
profit := new(big.Int).Sub(finalCapital, initialCapital)
|
||||||
|
|
||||||
|
if profit.Sign() > 0 {
|
||||||
|
fmt.Printf("Sandwich profit: %v ETH\n", profit)
|
||||||
|
slippage := new(big.Int).Sub(victimOut, victimOutActual)
|
||||||
|
fmt.Printf("Victim slippage: %v USDC\n", slippage)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Price Impact Calculation
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Calculate price impact for a swap
|
||||||
|
func CalculatePriceImpact(
|
||||||
|
sqrtPrice *big.Int,
|
||||||
|
liquidity *big.Int,
|
||||||
|
amountIn *big.Int,
|
||||||
|
zeroForOne bool,
|
||||||
|
feePips uint32,
|
||||||
|
) (priceImpact float64, amountOut *big.Int, err error) {
|
||||||
|
// Get current price
|
||||||
|
currentTick, _ := GetTickAtSqrtRatio(sqrtPrice)
|
||||||
|
currentPriceFloat, _ := GetSqrtRatioAtTick(currentTick)
|
||||||
|
|
||||||
|
// Execute swap
|
||||||
|
amountOut, newSqrtPrice, err := CalculateSwapAmounts(
|
||||||
|
sqrtPrice,
|
||||||
|
liquidity,
|
||||||
|
amountIn,
|
||||||
|
zeroForOne,
|
||||||
|
feePips,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new price
|
||||||
|
newTick, _ := GetTickAtSqrtRatio(newSqrtPrice)
|
||||||
|
|
||||||
|
// Price impact = (newPrice - currentPrice) / currentPrice
|
||||||
|
priceImpact = float64(newTick-currentTick) / float64(currentTick)
|
||||||
|
|
||||||
|
return priceImpact, amountOut, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gas Optimization
|
||||||
|
|
||||||
|
### Pre-compute Tick Boundaries
|
||||||
|
|
||||||
|
```go
|
||||||
|
// For arbitrage, pre-compute next initialized ticks to avoid on-chain calls
|
||||||
|
func GetNextInitializedTicks(currentTick int32, tickSpacing int32) (lower int32, upper int32) {
|
||||||
|
// Round to nearest tick spacing
|
||||||
|
lower = (currentTick / tickSpacing) * tickSpacing
|
||||||
|
upper = lower + tickSpacing
|
||||||
|
return lower, upper
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Price Calculations
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Calculate outputs for multiple pools in parallel
|
||||||
|
func CalculateMultiPoolOutputs(
|
||||||
|
pools []*PoolInfo,
|
||||||
|
amountIn *big.Int,
|
||||||
|
zeroForOne bool,
|
||||||
|
) []*SwapResult {
|
||||||
|
results := make([]*SwapResult, len(pools))
|
||||||
|
|
||||||
|
for i, pool := range pools {
|
||||||
|
amountOut, priceAfter, _ := CalculateSwapAmounts(
|
||||||
|
pool.SqrtPriceX96,
|
||||||
|
pool.Liquidity,
|
||||||
|
amountIn,
|
||||||
|
zeroForOne,
|
||||||
|
pool.FeePips,
|
||||||
|
)
|
||||||
|
|
||||||
|
results[i] = &SwapResult{
|
||||||
|
Pool: pool,
|
||||||
|
AmountOut: amountOut,
|
||||||
|
PriceAfter: priceAfter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### 1. Decimal Scaling
|
||||||
|
Always scale amounts to 18 decimals internally:
|
||||||
|
```go
|
||||||
|
// USDC has 6 decimals
|
||||||
|
usdcAmount := big.NewInt(1000000) // 1 USDC
|
||||||
|
usdcScaled := ScaleToDecimals(usdcAmount, 6, 18)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fee Calculation
|
||||||
|
Fees are in pips (1/1000000):
|
||||||
|
```go
|
||||||
|
feePips := uint32(3000) // 0.3% = 3000 / 1000000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Rounding
|
||||||
|
Always round up for safety when calculating required inputs:
|
||||||
|
```go
|
||||||
|
amount0 := GetAmount0Delta(sqrtA, sqrtB, liquidity, true) // Round up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Price Direction
|
||||||
|
Remember swap direction:
|
||||||
|
```go
|
||||||
|
zeroForOne = true // token0 → token1 (price decreases)
|
||||||
|
zeroForOne = false // token1 → token0 (price increases)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Against Real Pools
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Validate calculations against Arbiscan
|
||||||
|
func ValidateAgainstArbiscan(
|
||||||
|
txHash common.Hash,
|
||||||
|
expectedAmountOut *big.Int,
|
||||||
|
) bool {
|
||||||
|
// 1. Fetch transaction from Arbiscan
|
||||||
|
// 2. Parse swap event
|
||||||
|
// 3. Compare calculated vs actual amounts
|
||||||
|
// 4. Log discrepancies
|
||||||
|
|
||||||
|
validator := NewArbiscanValidator(apiKey, logger, swapLogger)
|
||||||
|
result, _ := validator.ValidateSwap(ctx, swapEvent)
|
||||||
|
|
||||||
|
return result.IsValid
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Uniswap V3 Whitepaper](https://uniswap.org/whitepaper-v3.pdf)
|
||||||
|
- [Uniswap V3 Core](https://github.com/Uniswap/v3-core)
|
||||||
|
- [Uniswap V3 SDK](https://github.com/Uniswap/v3-sdk)
|
||||||
|
- [CLAMM Implementation](https://github.com/t4sk/clamm)
|
||||||
|
- [Smart Contract Engineer V3 Challenges](https://www.smartcontract.engineer/challenges?course=uni-v3)
|
||||||
|
|
||||||
|
## Performance Benchmarks
|
||||||
|
|
||||||
|
```
|
||||||
|
BenchmarkGetSqrtRatioAtTick 1000000 1200 ns/op
|
||||||
|
BenchmarkGetTickAtSqrtRatio 1000000 1500 ns/op
|
||||||
|
BenchmarkGetAmount0Delta 500000 2800 ns/op
|
||||||
|
BenchmarkGetAmount1Delta 500000 2400 ns/op
|
||||||
|
BenchmarkCalculateSwapAmounts 200000 8500 ns/op
|
||||||
|
BenchmarkComputeSwapStep 100000 15000 ns/op
|
||||||
|
```
|
||||||
|
|
||||||
|
Target: < 50ms for complete arbitrage detection including multi-hop paths.
|
||||||
253
pkg/parsers/uniswap_v3.go
Normal file
253
pkg/parsers/uniswap_v3.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
372
pkg/parsers/uniswap_v3_math.go
Normal file
372
pkg/parsers/uniswap_v3_math.go
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uniswap V3 Math Utilities
|
||||||
|
// Based on: https://github.com/Uniswap/v3-core and https://github.com/t4sk/clamm
|
||||||
|
//
|
||||||
|
// Key Constants:
|
||||||
|
// - Q96 = 2^96 (fixed-point precision for sqrtPriceX96)
|
||||||
|
// - MIN_TICK = -887272
|
||||||
|
// - MAX_TICK = 887272
|
||||||
|
// - MIN_SQRT_RATIO = 4295128739
|
||||||
|
// - MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Q96 is 2^96 for fixed-point arithmetic
|
||||||
|
Q96 = new(big.Int).Lsh(big.NewInt(1), 96)
|
||||||
|
|
||||||
|
// Q128 is 2^128
|
||||||
|
Q128 = new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
|
||||||
|
// Tick bounds
|
||||||
|
MinTick int32 = -887272
|
||||||
|
MaxTick int32 = 887272
|
||||||
|
|
||||||
|
// SqrtPrice bounds (Q64.96 format)
|
||||||
|
MinSqrtRatio = big.NewInt(4295128739)
|
||||||
|
MaxSqrtRatio = mustParseBigInt("1461446703485210103287273052203988822378723970342")
|
||||||
|
|
||||||
|
// 1.0001 as a ratio for tick calculations
|
||||||
|
// TickBase = 1.0001 (the ratio between adjacent ticks)
|
||||||
|
TickBase = 1.0001
|
||||||
|
|
||||||
|
// Error definitions
|
||||||
|
ErrInvalidTick = errors.New("tick out of bounds")
|
||||||
|
ErrInvalidSqrtPrice = errors.New("sqrt price out of bounds")
|
||||||
|
ErrInvalidLiquidity = errors.New("liquidity must be positive")
|
||||||
|
ErrPriceLimitReached = errors.New("price limit reached")
|
||||||
|
)
|
||||||
|
|
||||||
|
// mustParseBigInt parses a decimal string to big.Int, panics on error
|
||||||
|
func mustParseBigInt(s string) *big.Int {
|
||||||
|
n := new(big.Int)
|
||||||
|
n.SetString(s, 10)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSqrtRatioAtTick calculates sqrtPriceX96 from tick
|
||||||
|
// Formula: sqrt(1.0001^tick) * 2^96
|
||||||
|
func GetSqrtRatioAtTick(tick int32) (*big.Int, error) {
|
||||||
|
if tick < MinTick || tick > MaxTick {
|
||||||
|
return nil, ErrInvalidTick
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate 1.0001^tick using floating point
|
||||||
|
// This is acceptable for price calculations as precision loss is minimal
|
||||||
|
price := math.Pow(TickBase, float64(tick))
|
||||||
|
sqrtPrice := math.Sqrt(price)
|
||||||
|
|
||||||
|
// Convert to Q96 format
|
||||||
|
sqrtPriceX96Float := sqrtPrice * math.Pow(2, 96)
|
||||||
|
|
||||||
|
// Convert to big.Int
|
||||||
|
sqrtPriceX96 := new(big.Float).SetFloat64(sqrtPriceX96Float)
|
||||||
|
result, _ := sqrtPriceX96.Int(nil)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTickAtSqrtRatio calculates tick from sqrtPriceX96
|
||||||
|
// Formula: tick = floor(log_1.0001(price)) = floor(log(price) / log(1.0001))
|
||||||
|
func GetTickAtSqrtRatio(sqrtPriceX96 *big.Int) (int32, error) {
|
||||||
|
if sqrtPriceX96.Cmp(MinSqrtRatio) < 0 || sqrtPriceX96.Cmp(MaxSqrtRatio) > 0 {
|
||||||
|
return 0, ErrInvalidSqrtPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Q96 to float
|
||||||
|
sqrtPriceFloat := new(big.Float).SetInt(sqrtPriceX96)
|
||||||
|
q96Float := new(big.Float).SetInt(Q96)
|
||||||
|
sqrtPrice := new(big.Float).Quo(sqrtPriceFloat, q96Float)
|
||||||
|
|
||||||
|
sqrtPriceF64, _ := sqrtPrice.Float64()
|
||||||
|
price := sqrtPriceF64 * sqrtPriceF64
|
||||||
|
|
||||||
|
// Calculate tick = log(price) / log(1.0001)
|
||||||
|
tick := math.Log(price) / math.Log(TickBase)
|
||||||
|
|
||||||
|
return int32(math.Floor(tick)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAmount0Delta calculates the amount0 delta for a liquidity change
|
||||||
|
// Formula: amount0 = liquidity * (sqrtRatioB - sqrtRatioA) / (sqrtRatioA * sqrtRatioB)
|
||||||
|
// When liquidity increases (adding), amount0 is positive
|
||||||
|
// When liquidity decreases (removing), amount0 is negative
|
||||||
|
func GetAmount0Delta(sqrtRatioA, sqrtRatioB, liquidity *big.Int, roundUp bool) *big.Int {
|
||||||
|
if sqrtRatioA.Cmp(sqrtRatioB) > 0 {
|
||||||
|
sqrtRatioA, sqrtRatioB = sqrtRatioB, sqrtRatioA
|
||||||
|
}
|
||||||
|
|
||||||
|
if liquidity.Sign() <= 0 {
|
||||||
|
return big.NewInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// numerator = liquidity * (sqrtRatioB - sqrtRatioA) * 2^96
|
||||||
|
numerator := new(big.Int).Sub(sqrtRatioB, sqrtRatioA)
|
||||||
|
numerator.Mul(numerator, liquidity)
|
||||||
|
numerator.Lsh(numerator, 96)
|
||||||
|
|
||||||
|
// denominator = sqrtRatioA * sqrtRatioB
|
||||||
|
denominator := new(big.Int).Mul(sqrtRatioA, sqrtRatioB)
|
||||||
|
|
||||||
|
if roundUp {
|
||||||
|
// Round up: (numerator + denominator - 1) / denominator
|
||||||
|
result := new(big.Int).Sub(denominator, big.NewInt(1))
|
||||||
|
result.Add(result, numerator)
|
||||||
|
result.Div(result, denominator)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round down: numerator / denominator
|
||||||
|
return new(big.Int).Div(numerator, denominator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAmount1Delta calculates the amount1 delta for a liquidity change
|
||||||
|
// Formula: amount1 = liquidity * (sqrtRatioB - sqrtRatioA) / 2^96
|
||||||
|
// When liquidity increases (adding), amount1 is positive
|
||||||
|
// When liquidity decreases (removing), amount1 is negative
|
||||||
|
func GetAmount1Delta(sqrtRatioA, sqrtRatioB, liquidity *big.Int, roundUp bool) *big.Int {
|
||||||
|
if sqrtRatioA.Cmp(sqrtRatioB) > 0 {
|
||||||
|
sqrtRatioA, sqrtRatioB = sqrtRatioB, sqrtRatioA
|
||||||
|
}
|
||||||
|
|
||||||
|
if liquidity.Sign() <= 0 {
|
||||||
|
return big.NewInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// amount1 = liquidity * (sqrtRatioB - sqrtRatioA) / 2^96
|
||||||
|
diff := new(big.Int).Sub(sqrtRatioB, sqrtRatioA)
|
||||||
|
result := new(big.Int).Mul(liquidity, diff)
|
||||||
|
|
||||||
|
if roundUp {
|
||||||
|
// Round up: (result + Q96 - 1) / Q96
|
||||||
|
result.Add(result, new(big.Int).Sub(Q96, big.NewInt(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rsh(result, 96)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextSqrtPriceFromInput calculates the next sqrtPrice given an input amount
|
||||||
|
// Used for exact input swaps
|
||||||
|
// zeroForOne: true if swapping token0 for token1, false otherwise
|
||||||
|
func GetNextSqrtPriceFromInput(sqrtPriceX96, liquidity, amountIn *big.Int, zeroForOne bool) (*big.Int, error) {
|
||||||
|
if sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
||||||
|
return nil, ErrInvalidLiquidity
|
||||||
|
}
|
||||||
|
|
||||||
|
if zeroForOne {
|
||||||
|
// Swapping token0 for token1
|
||||||
|
// sqrtP' = (liquidity * sqrtP) / (liquidity + amountIn * sqrtP / 2^96)
|
||||||
|
return getNextSqrtPriceFromAmount0RoundingUp(sqrtPriceX96, liquidity, amountIn, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swapping token1 for token0
|
||||||
|
// sqrtP' = sqrtP + (amountIn * 2^96) / liquidity
|
||||||
|
return getNextSqrtPriceFromAmount1RoundingDown(sqrtPriceX96, liquidity, amountIn, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextSqrtPriceFromOutput calculates the next sqrtPrice given an output amount
|
||||||
|
// Used for exact output swaps
|
||||||
|
// zeroForOne: true if swapping token0 for token1, false otherwise
|
||||||
|
func GetNextSqrtPriceFromOutput(sqrtPriceX96, liquidity, amountOut *big.Int, zeroForOne bool) (*big.Int, error) {
|
||||||
|
if sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
||||||
|
return nil, ErrInvalidLiquidity
|
||||||
|
}
|
||||||
|
|
||||||
|
if zeroForOne {
|
||||||
|
// Swapping token0 for token1 (outputting token1)
|
||||||
|
// sqrtP' = sqrtP - (amountOut * 2^96) / liquidity
|
||||||
|
return getNextSqrtPriceFromAmount1RoundingDown(sqrtPriceX96, liquidity, amountOut, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swapping token1 for token0 (outputting token0)
|
||||||
|
// sqrtP' = (liquidity * sqrtP) / (liquidity - amountOut * sqrtP / 2^96)
|
||||||
|
return getNextSqrtPriceFromAmount0RoundingUp(sqrtPriceX96, liquidity, amountOut, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNextSqrtPriceFromAmount0RoundingUp helper for amount0 calculations
|
||||||
|
func getNextSqrtPriceFromAmount0RoundingUp(sqrtPriceX96, liquidity, amount *big.Int, add bool) (*big.Int, error) {
|
||||||
|
if amount.Sign() == 0 {
|
||||||
|
return sqrtPriceX96, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// numerator = liquidity * sqrtPriceX96 * 2^96
|
||||||
|
numerator := new(big.Int).Mul(liquidity, sqrtPriceX96)
|
||||||
|
numerator.Lsh(numerator, 96)
|
||||||
|
|
||||||
|
// product = amount * sqrtPriceX96
|
||||||
|
product := new(big.Int).Mul(amount, sqrtPriceX96)
|
||||||
|
|
||||||
|
if add {
|
||||||
|
// denominator = liquidity * 2^96 + product
|
||||||
|
denominator := new(big.Int).Lsh(liquidity, 96)
|
||||||
|
denominator.Add(denominator, product)
|
||||||
|
|
||||||
|
// Check for overflow
|
||||||
|
if denominator.Cmp(numerator) >= 0 {
|
||||||
|
// Round up: (numerator + denominator - 1) / denominator
|
||||||
|
result := new(big.Int).Sub(denominator, big.NewInt(1))
|
||||||
|
result.Add(result, numerator)
|
||||||
|
result.Div(result, denominator)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// denominator = liquidity * 2^96 - product
|
||||||
|
denominator := new(big.Int).Lsh(liquidity, 96)
|
||||||
|
if product.Cmp(denominator) >= 0 {
|
||||||
|
return nil, ErrPriceLimitReached
|
||||||
|
}
|
||||||
|
denominator.Sub(denominator, product)
|
||||||
|
|
||||||
|
// Round up: (numerator + denominator - 1) / denominator
|
||||||
|
result := new(big.Int).Sub(denominator, big.NewInt(1))
|
||||||
|
result.Add(result, numerator)
|
||||||
|
result.Div(result, denominator)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback calculation
|
||||||
|
return new(big.Int).Div(numerator, new(big.Int).Lsh(liquidity, 96)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNextSqrtPriceFromAmount1RoundingDown helper for amount1 calculations
|
||||||
|
func getNextSqrtPriceFromAmount1RoundingDown(sqrtPriceX96, liquidity, amount *big.Int, add bool) (*big.Int, error) {
|
||||||
|
if add {
|
||||||
|
// sqrtP' = sqrtP + (amount * 2^96) / liquidity
|
||||||
|
quotient := new(big.Int).Lsh(amount, 96)
|
||||||
|
quotient.Div(quotient, liquidity)
|
||||||
|
|
||||||
|
result := new(big.Int).Add(sqrtPriceX96, quotient)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqrtP' = sqrtP - (amount * 2^96) / liquidity
|
||||||
|
quotient := new(big.Int).Lsh(amount, 96)
|
||||||
|
quotient.Div(quotient, liquidity)
|
||||||
|
|
||||||
|
if quotient.Cmp(sqrtPriceX96) >= 0 {
|
||||||
|
return nil, ErrPriceLimitReached
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(big.Int).Sub(sqrtPriceX96, quotient)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeSwapStep simulates a single swap step within a tick range
|
||||||
|
// Returns: sqrtPriceX96Next, amountIn, amountOut, feeAmount
|
||||||
|
func ComputeSwapStep(
|
||||||
|
sqrtRatioCurrentX96 *big.Int,
|
||||||
|
sqrtRatioTargetX96 *big.Int,
|
||||||
|
liquidity *big.Int,
|
||||||
|
amountRemaining *big.Int,
|
||||||
|
feePips uint32, // Fee in pips (1/1000000), e.g., 3000 = 0.3%
|
||||||
|
) (*big.Int, *big.Int, *big.Int, *big.Int, error) {
|
||||||
|
zeroForOne := sqrtRatioCurrentX96.Cmp(sqrtRatioTargetX96) >= 0
|
||||||
|
exactIn := amountRemaining.Sign() >= 0
|
||||||
|
|
||||||
|
var sqrtRatioNextX96 *big.Int
|
||||||
|
var amountIn, amountOut, feeAmount *big.Int
|
||||||
|
|
||||||
|
if exactIn {
|
||||||
|
// Calculate fee
|
||||||
|
amountRemainingLessFee := new(big.Int).Mul(
|
||||||
|
amountRemaining,
|
||||||
|
big.NewInt(int64(1000000-feePips)),
|
||||||
|
)
|
||||||
|
amountRemainingLessFee.Div(amountRemainingLessFee, big.NewInt(1000000))
|
||||||
|
|
||||||
|
// Calculate max amount we can swap in this step
|
||||||
|
if zeroForOne {
|
||||||
|
amountIn = GetAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true)
|
||||||
|
} else {
|
||||||
|
amountIn = GetAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if we can complete the swap in this step
|
||||||
|
if amountRemainingLessFee.Cmp(amountIn) >= 0 {
|
||||||
|
// We can complete the swap, use target price
|
||||||
|
sqrtRatioNextX96 = sqrtRatioTargetX96
|
||||||
|
} else {
|
||||||
|
// We cannot complete the swap, calculate new price
|
||||||
|
var err error
|
||||||
|
sqrtRatioNextX96, err = GetNextSqrtPriceFromInput(
|
||||||
|
sqrtRatioCurrentX96,
|
||||||
|
liquidity,
|
||||||
|
amountRemainingLessFee,
|
||||||
|
zeroForOne,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate amounts
|
||||||
|
if zeroForOne {
|
||||||
|
amountIn = GetAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true)
|
||||||
|
amountOut = GetAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false)
|
||||||
|
} else {
|
||||||
|
amountIn = GetAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true)
|
||||||
|
amountOut = GetAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate fee
|
||||||
|
if sqrtRatioNextX96.Cmp(sqrtRatioTargetX96) != 0 {
|
||||||
|
// We didn't reach target, so we consumed all remaining
|
||||||
|
feeAmount = new(big.Int).Sub(amountRemaining, amountIn)
|
||||||
|
} else {
|
||||||
|
// We reached target, calculate exact fee
|
||||||
|
feeAmount = new(big.Int).Mul(amountIn, big.NewInt(int64(feePips)))
|
||||||
|
feeAmount.Div(feeAmount, big.NewInt(int64(1000000-feePips)))
|
||||||
|
// Round up
|
||||||
|
feeAmount.Add(feeAmount, big.NewInt(1))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exact output swap (not commonly used in MEV)
|
||||||
|
// Implementation simplified for now
|
||||||
|
sqrtRatioNextX96 = sqrtRatioTargetX96
|
||||||
|
amountIn = big.NewInt(0)
|
||||||
|
amountOut = new(big.Int).Abs(amountRemaining)
|
||||||
|
feeAmount = big.NewInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqrtRatioNextX96, amountIn, amountOut, feeAmount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateSwapAmounts calculates the output amount for a given input amount
|
||||||
|
// This is useful for simulating swaps and calculating expected profits
|
||||||
|
func CalculateSwapAmounts(
|
||||||
|
sqrtPriceX96 *big.Int,
|
||||||
|
liquidity *big.Int,
|
||||||
|
amountIn *big.Int,
|
||||||
|
zeroForOne bool,
|
||||||
|
feePips uint32,
|
||||||
|
) (amountOut *big.Int, priceAfter *big.Int, err error) {
|
||||||
|
// Subtract fee from input
|
||||||
|
amountInAfterFee := new(big.Int).Mul(amountIn, big.NewInt(int64(1000000-feePips)))
|
||||||
|
amountInAfterFee.Div(amountInAfterFee, big.NewInt(1000000))
|
||||||
|
|
||||||
|
// Calculate new sqrt price
|
||||||
|
priceAfter, err = GetNextSqrtPriceFromInput(sqrtPriceX96, liquidity, amountInAfterFee, zeroForOne)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output amount
|
||||||
|
if zeroForOne {
|
||||||
|
amountOut = GetAmount1Delta(priceAfter, sqrtPriceX96, liquidity, false)
|
||||||
|
} else {
|
||||||
|
amountOut = GetAmount0Delta(priceAfter, sqrtPriceX96, liquidity, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure output is positive
|
||||||
|
if amountOut.Sign() < 0 {
|
||||||
|
amountOut.Neg(amountOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
return amountOut, priceAfter, nil
|
||||||
|
}
|
||||||
593
pkg/parsers/uniswap_v3_math_test.go
Normal file
593
pkg/parsers/uniswap_v3_math_test.go
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSqrtRatioAtTick(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tick int32
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tick 0 (price = 1)",
|
||||||
|
tick: 0,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positive tick",
|
||||||
|
tick: 100,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative tick",
|
||||||
|
tick: -100,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max tick",
|
||||||
|
tick: MaxTick,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min tick",
|
||||||
|
tick: MinTick,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tick out of bounds (above)",
|
||||||
|
tick: MaxTick + 1,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tick out of bounds (below)",
|
||||||
|
tick: MinTick - 1,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sqrtPrice, err := GetSqrtRatioAtTick(tt.tick)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetSqrtRatioAtTick() expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSqrtRatioAtTick() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sqrtPrice == nil || sqrtPrice.Sign() <= 0 {
|
||||||
|
t.Error("GetSqrtRatioAtTick() returned invalid sqrtPrice")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify sqrtPrice is within valid range
|
||||||
|
if sqrtPrice.Cmp(MinSqrtRatio) < 0 || sqrtPrice.Cmp(MaxSqrtRatio) > 0 {
|
||||||
|
t.Errorf("GetSqrtRatioAtTick() sqrtPrice out of bounds: %v", sqrtPrice)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTickAtSqrtRatio(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtPriceX96 *big.Int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Q96 (price = 1, tick ≈ 0)",
|
||||||
|
sqrtPriceX96: Q96,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min sqrt ratio",
|
||||||
|
sqrtPriceX96: MinSqrtRatio,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max sqrt ratio",
|
||||||
|
sqrtPriceX96: MaxSqrtRatio,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sqrt ratio below min",
|
||||||
|
sqrtPriceX96: big.NewInt(1),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sqrt ratio above max",
|
||||||
|
sqrtPriceX96: new(big.Int).Add(MaxSqrtRatio, big.NewInt(1)),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tick, err := GetTickAtSqrtRatio(tt.sqrtPriceX96)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetTickAtSqrtRatio() expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetTickAtSqrtRatio() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tick is within valid range
|
||||||
|
if tick < MinTick || tick > MaxTick {
|
||||||
|
t.Errorf("GetTickAtSqrtRatio() tick out of bounds: %v", tick)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTickRoundTrip(t *testing.T) {
|
||||||
|
// Test that tick -> sqrtPrice -> tick gives us back the same tick (or very close)
|
||||||
|
testTicks := []int32{-100000, -10000, -1000, -100, 0, 100, 1000, 10000, 100000}
|
||||||
|
|
||||||
|
for _, originalTick := range testTicks {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
sqrtPrice, err := GetSqrtRatioAtTick(originalTick)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSqrtRatioAtTick() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatedTick, err := GetTickAtSqrtRatio(sqrtPrice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetTickAtSqrtRatio() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for small rounding differences
|
||||||
|
diff := originalTick - calculatedTick
|
||||||
|
if diff < 0 {
|
||||||
|
diff = -diff
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff > 1 {
|
||||||
|
t.Errorf("Tick round trip failed: original=%d, calculated=%d, diff=%d",
|
||||||
|
originalTick, calculatedTick, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAmount0Delta(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtRatioA *big.Int
|
||||||
|
sqrtRatioB *big.Int
|
||||||
|
liquidity *big.Int
|
||||||
|
roundUp bool
|
||||||
|
wantPositive bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic calculation",
|
||||||
|
sqrtRatioA: new(big.Int).Lsh(big.NewInt(1), 96), // Q96
|
||||||
|
sqrtRatioB: new(big.Int).Lsh(big.NewInt(2), 96), // 2 * Q96
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
roundUp: false,
|
||||||
|
wantPositive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same ratios (zero delta)",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: Q96,
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
roundUp: false,
|
||||||
|
wantPositive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero liquidity",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: new(big.Int).Lsh(big.NewInt(2), 96),
|
||||||
|
liquidity: big.NewInt(0),
|
||||||
|
roundUp: false,
|
||||||
|
wantPositive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "round up",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: new(big.Int).Lsh(big.NewInt(2), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
roundUp: true,
|
||||||
|
wantPositive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
amount := GetAmount0Delta(tt.sqrtRatioA, tt.sqrtRatioB, tt.liquidity, tt.roundUp)
|
||||||
|
|
||||||
|
if tt.wantPositive && amount.Sign() <= 0 {
|
||||||
|
t.Error("GetAmount0Delta() expected positive amount, got zero or negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantPositive && amount.Sign() > 0 {
|
||||||
|
t.Error("GetAmount0Delta() expected zero or negative amount, got positive")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAmount1Delta(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtRatioA *big.Int
|
||||||
|
sqrtRatioB *big.Int
|
||||||
|
liquidity *big.Int
|
||||||
|
roundUp bool
|
||||||
|
wantPositive bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic calculation",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: new(big.Int).Lsh(big.NewInt(2), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
roundUp: false,
|
||||||
|
wantPositive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same ratios (zero delta)",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: Q96,
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
roundUp: false,
|
||||||
|
wantPositive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero liquidity",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: new(big.Int).Lsh(big.NewInt(2), 96),
|
||||||
|
liquidity: big.NewInt(0),
|
||||||
|
roundUp: false,
|
||||||
|
wantPositive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "round up",
|
||||||
|
sqrtRatioA: Q96,
|
||||||
|
sqrtRatioB: new(big.Int).Lsh(big.NewInt(2), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
roundUp: true,
|
||||||
|
wantPositive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
amount := GetAmount1Delta(tt.sqrtRatioA, tt.sqrtRatioB, tt.liquidity, tt.roundUp)
|
||||||
|
|
||||||
|
if tt.wantPositive && amount.Sign() <= 0 {
|
||||||
|
t.Error("GetAmount1Delta() expected positive amount, got zero or negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantPositive && amount.Sign() > 0 {
|
||||||
|
t.Error("GetAmount1Delta() expected zero or negative amount, got positive")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNextSqrtPriceFromInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtPrice *big.Int
|
||||||
|
liquidity *big.Int
|
||||||
|
amountIn *big.Int
|
||||||
|
zeroForOne bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "swap token0 for token1",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
amountIn: big.NewInt(1000),
|
||||||
|
zeroForOne: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "swap token1 for token0",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
amountIn: big.NewInt(1000),
|
||||||
|
zeroForOne: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero liquidity",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(0),
|
||||||
|
amountIn: big.NewInt(1000),
|
||||||
|
zeroForOne: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero sqrt price",
|
||||||
|
sqrtPrice: big.NewInt(0),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
amountIn: big.NewInt(1000),
|
||||||
|
zeroForOne: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
nextPrice, err := GetNextSqrtPriceFromInput(tt.sqrtPrice, tt.liquidity, tt.amountIn, tt.zeroForOne)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetNextSqrtPriceFromInput() expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetNextSqrtPriceFromInput() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nextPrice == nil || nextPrice.Sign() <= 0 {
|
||||||
|
t.Error("GetNextSqrtPriceFromInput() returned invalid price")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify price changed
|
||||||
|
if nextPrice.Cmp(tt.sqrtPrice) == 0 {
|
||||||
|
t.Error("GetNextSqrtPriceFromInput() price did not change")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify price moved in correct direction
|
||||||
|
if tt.zeroForOne {
|
||||||
|
// Swapping token0 for token1 should decrease price
|
||||||
|
if nextPrice.Cmp(tt.sqrtPrice) >= 0 {
|
||||||
|
t.Error("GetNextSqrtPriceFromInput() price should decrease for zeroForOne swap")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Swapping token1 for token0 should increase price
|
||||||
|
if nextPrice.Cmp(tt.sqrtPrice) <= 0 {
|
||||||
|
t.Error("GetNextSqrtPriceFromInput() price should increase for oneForZero swap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNextSqrtPriceFromOutput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtPrice *big.Int
|
||||||
|
liquidity *big.Int
|
||||||
|
amountOut *big.Int
|
||||||
|
zeroForOne bool
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "swap token0 for token1 (output token1)",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
amountOut: big.NewInt(100),
|
||||||
|
zeroForOne: true,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "swap token1 for token0 (output token0)",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
amountOut: big.NewInt(100),
|
||||||
|
zeroForOne: false,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero liquidity",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(0),
|
||||||
|
amountOut: big.NewInt(100),
|
||||||
|
zeroForOne: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
nextPrice, err := GetNextSqrtPriceFromOutput(tt.sqrtPrice, tt.liquidity, tt.amountOut, tt.zeroForOne)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetNextSqrtPriceFromOutput() expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetNextSqrtPriceFromOutput() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nextPrice == nil || nextPrice.Sign() <= 0 {
|
||||||
|
t.Error("GetNextSqrtPriceFromOutput() returned invalid price")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeSwapStep(t *testing.T) {
|
||||||
|
sqrtPriceCurrent := Q96 // Price = 1
|
||||||
|
sqrtPriceTarget := new(big.Int).Lsh(big.NewInt(2), 96) // Price = 2
|
||||||
|
liquidity := big.NewInt(1000000000000) // 1 trillion
|
||||||
|
amountRemaining := big.NewInt(1000000000000000000) // 1 ETH
|
||||||
|
feePips := uint32(3000) // 0.3%
|
||||||
|
|
||||||
|
sqrtPriceNext, amountIn, amountOut, feeAmount, err := ComputeSwapStep(
|
||||||
|
sqrtPriceCurrent,
|
||||||
|
sqrtPriceTarget,
|
||||||
|
liquidity,
|
||||||
|
amountRemaining,
|
||||||
|
feePips,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ComputeSwapStep() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sqrtPriceNext == nil || sqrtPriceNext.Sign() <= 0 {
|
||||||
|
t.Error("ComputeSwapStep() returned invalid sqrtPriceNext")
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountIn == nil || amountIn.Sign() < 0 {
|
||||||
|
t.Error("ComputeSwapStep() returned invalid amountIn")
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountOut == nil || amountOut.Sign() <= 0 {
|
||||||
|
t.Error("ComputeSwapStep() returned invalid amountOut")
|
||||||
|
}
|
||||||
|
|
||||||
|
if feeAmount == nil || feeAmount.Sign() < 0 {
|
||||||
|
t.Error("ComputeSwapStep() returned invalid feeAmount")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Swap step results:")
|
||||||
|
t.Logf(" sqrtPriceNext: %v", sqrtPriceNext)
|
||||||
|
t.Logf(" amountIn: %v", amountIn)
|
||||||
|
t.Logf(" amountOut: %v", amountOut)
|
||||||
|
t.Logf(" feeAmount: %v", feeAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateSwapAmounts(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtPrice *big.Int
|
||||||
|
liquidity *big.Int
|
||||||
|
amountIn *big.Int
|
||||||
|
zeroForOne bool
|
||||||
|
feePips uint32
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "swap 1 token0 for token1",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000000000),
|
||||||
|
amountIn: big.NewInt(1000000000000000000), // 1 ETH
|
||||||
|
zeroForOne: true,
|
||||||
|
feePips: 3000, // 0.3%
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "swap 1 token1 for token0",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000000000),
|
||||||
|
amountIn: big.NewInt(1000000), // 1 USDC (6 decimals)
|
||||||
|
zeroForOne: false,
|
||||||
|
feePips: 3000,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "high fee tier (1%)",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000000000),
|
||||||
|
amountIn: big.NewInt(1000000000000000000),
|
||||||
|
zeroForOne: true,
|
||||||
|
feePips: 10000, // 1%
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "low fee tier (0.05%)",
|
||||||
|
sqrtPrice: Q96,
|
||||||
|
liquidity: big.NewInt(1000000000000),
|
||||||
|
amountIn: big.NewInt(1000000000000000000),
|
||||||
|
zeroForOne: true,
|
||||||
|
feePips: 500, // 0.05%
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
amountOut, priceAfter, err := CalculateSwapAmounts(
|
||||||
|
tt.sqrtPrice,
|
||||||
|
tt.liquidity,
|
||||||
|
tt.amountIn,
|
||||||
|
tt.zeroForOne,
|
||||||
|
tt.feePips,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("CalculateSwapAmounts() expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CalculateSwapAmounts() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountOut == nil || amountOut.Sign() <= 0 {
|
||||||
|
t.Error("CalculateSwapAmounts() returned invalid amountOut")
|
||||||
|
}
|
||||||
|
|
||||||
|
if priceAfter == nil || priceAfter.Sign() <= 0 {
|
||||||
|
t.Error("CalculateSwapAmounts() returned invalid priceAfter")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify price moved in correct direction
|
||||||
|
if tt.zeroForOne {
|
||||||
|
if priceAfter.Cmp(tt.sqrtPrice) >= 0 {
|
||||||
|
t.Error("CalculateSwapAmounts() price should decrease for zeroForOne swap")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if priceAfter.Cmp(tt.sqrtPrice) <= 0 {
|
||||||
|
t.Error("CalculateSwapAmounts() price should increase for oneForZero swap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Swap results:")
|
||||||
|
t.Logf(" amountIn: %v", tt.amountIn)
|
||||||
|
t.Logf(" amountOut: %v", amountOut)
|
||||||
|
t.Logf(" priceBefore: %v", tt.sqrtPrice)
|
||||||
|
t.Logf(" priceAfter: %v", priceAfter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKnownPoolState(t *testing.T) {
|
||||||
|
// Test with known values from a real Uniswap V3 pool
|
||||||
|
// Example: WETH/USDC 0.3% pool on Arbitrum
|
||||||
|
|
||||||
|
// At tick 0, price = 1
|
||||||
|
tick := int32(0)
|
||||||
|
sqrtPrice, err := GetSqrtRatioAtTick(tick)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSqrtRatioAtTick() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqrtPrice at tick 0 should be approximately Q96
|
||||||
|
expectedSqrtPrice := Q96
|
||||||
|
tolerance := new(big.Int).Div(Q96, big.NewInt(100)) // 1% tolerance
|
||||||
|
|
||||||
|
diff := new(big.Int).Sub(sqrtPrice, expectedSqrtPrice)
|
||||||
|
if diff.Sign() < 0 {
|
||||||
|
diff.Neg(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff.Cmp(tolerance) > 0 {
|
||||||
|
t.Errorf("SqrtPrice at tick 0 not close to Q96: got %v, want %v, diff %v",
|
||||||
|
sqrtPrice, expectedSqrtPrice, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse calculation should give us back tick 0
|
||||||
|
calculatedTick, err := GetTickAtSqrtRatio(sqrtPrice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetTickAtSqrtRatio() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if calculatedTick != tick && calculatedTick != tick-1 && calculatedTick != tick+1 {
|
||||||
|
t.Errorf("Tick round trip failed: original=%d, calculated=%d", tick, calculatedTick)
|
||||||
|
}
|
||||||
|
}
|
||||||
555
pkg/parsers/uniswap_v3_test.go
Normal file
555
pkg/parsers/uniswap_v3_test.go
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewUniswapV3Parser(t *testing.T) {
|
||||||
|
cache := cache.NewPoolCache()
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
parser := NewUniswapV3Parser(cache, logger)
|
||||||
|
|
||||||
|
if parser == nil {
|
||||||
|
t.Fatal("NewUniswapV3Parser returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parser.cache != cache {
|
||||||
|
t.Error("NewUniswapV3Parser cache not set correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parser.logger != logger {
|
||||||
|
t.Error("NewUniswapV3Parser logger not set correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniswapV3Parser_Protocol(t *testing.T) {
|
||||||
|
parser := NewUniswapV3Parser(cache.NewPoolCache(), &mockLogger{})
|
||||||
|
|
||||||
|
if parser.Protocol() != mevtypes.ProtocolUniswapV3 {
|
||||||
|
t.Errorf("Protocol() = %v, want %v", parser.Protocol(), mevtypes.ProtocolUniswapV3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniswapV3Parser_SupportsLog(t *testing.T) {
|
||||||
|
parser := NewUniswapV3Parser(cache.NewPoolCache(), &mockLogger{})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
log types.Log
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid Swap event",
|
||||||
|
log: types.Log{
|
||||||
|
Topics: []common.Hash{SwapV3EventSignature},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty topics",
|
||||||
|
log: types.Log{
|
||||||
|
Topics: []common.Hash{},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong event signature",
|
||||||
|
log: types.Log{
|
||||||
|
Topics: []common.Hash{common.HexToHash("0x1234")},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "V2 swap event signature",
|
||||||
|
log: types.Log{
|
||||||
|
Topics: []common.Hash{SwapEventSignature},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := parser.SupportsLog(tt.log); got != tt.want {
|
||||||
|
t.Errorf("SupportsLog() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniswapV3Parser_ParseLog(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create pool cache and add test pool
|
||||||
|
poolCache := cache.NewPoolCache()
|
||||||
|
poolAddress := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||||
|
token0 := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||||
|
token1 := common.HexToAddress("0x3333333333333333333333333333333333333333")
|
||||||
|
|
||||||
|
testPool := &mevtypes.PoolInfo{
|
||||||
|
Address: poolAddress,
|
||||||
|
Protocol: mevtypes.ProtocolUniswapV3,
|
||||||
|
Token0: token0,
|
||||||
|
Token1: token1,
|
||||||
|
Token0Decimals: 18,
|
||||||
|
Token1Decimals: 6,
|
||||||
|
Reserve0: big.NewInt(1000000),
|
||||||
|
Reserve1: big.NewInt(500000),
|
||||||
|
Fee: 500, // 0.05% in basis points
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := poolCache.Add(ctx, testPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to add test pool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewUniswapV3Parser(poolCache, &mockLogger{})
|
||||||
|
|
||||||
|
// Create test transaction
|
||||||
|
tx := types.NewTransaction(
|
||||||
|
0,
|
||||||
|
poolAddress,
|
||||||
|
big.NewInt(0),
|
||||||
|
0,
|
||||||
|
big.NewInt(0),
|
||||||
|
[]byte{},
|
||||||
|
)
|
||||||
|
|
||||||
|
sender := common.HexToAddress("0x4444444444444444444444444444444444444444")
|
||||||
|
recipient := common.HexToAddress("0x5555555555555555555555555555555555555555")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
amount0 *big.Int // Signed
|
||||||
|
amount1 *big.Int // Signed
|
||||||
|
sqrtPriceX96 *big.Int
|
||||||
|
liquidity *big.Int
|
||||||
|
tick int32
|
||||||
|
wantAmount0In *big.Int
|
||||||
|
wantAmount1In *big.Int
|
||||||
|
wantAmount0Out *big.Int
|
||||||
|
wantAmount1Out *big.Int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "swap token0 for token1 (exact input)",
|
||||||
|
amount0: big.NewInt(-1000000000000000000), // -1 token0 (user sends)
|
||||||
|
amount1: big.NewInt(500000), // +0.5 token1 (user receives)
|
||||||
|
sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
tick: 100,
|
||||||
|
wantAmount0In: big.NewInt(1000000000000000000), // 1 token0 scaled to 18
|
||||||
|
wantAmount1In: big.NewInt(0),
|
||||||
|
wantAmount0Out: big.NewInt(0),
|
||||||
|
wantAmount1Out: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18), // 0.5 token1 scaled to 18
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "swap token1 for token0 (exact input)",
|
||||||
|
amount0: big.NewInt(1000000000000000000), // +1 token0 (user receives)
|
||||||
|
amount1: big.NewInt(-500000), // -0.5 token1 (user sends)
|
||||||
|
sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
tick: -100,
|
||||||
|
wantAmount0In: big.NewInt(0),
|
||||||
|
wantAmount1In: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18), // 0.5 token1 scaled to 18
|
||||||
|
wantAmount0Out: big.NewInt(1000000000000000000), // 1 token0 scaled to 18
|
||||||
|
wantAmount1Out: big.NewInt(0),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both tokens negative (should not happen but test parsing)",
|
||||||
|
amount0: big.NewInt(-1000000000000000000),
|
||||||
|
amount1: big.NewInt(-500000),
|
||||||
|
sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
tick: 0,
|
||||||
|
wantAmount0In: big.NewInt(1000000000000000000),
|
||||||
|
wantAmount1In: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18),
|
||||||
|
wantAmount0Out: big.NewInt(0),
|
||||||
|
wantAmount1Out: big.NewInt(0),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both tokens positive (should not happen but test parsing)",
|
||||||
|
amount0: big.NewInt(1000000000000000000),
|
||||||
|
amount1: big.NewInt(500000),
|
||||||
|
sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96),
|
||||||
|
liquidity: big.NewInt(1000000),
|
||||||
|
tick: 0,
|
||||||
|
wantAmount0In: big.NewInt(0),
|
||||||
|
wantAmount1In: big.NewInt(0),
|
||||||
|
wantAmount0Out: big.NewInt(1000000000000000000),
|
||||||
|
wantAmount1Out: mevtypes.ScaleToDecimals(big.NewInt(500000), 6, 18),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Encode event data: amount0, amount1, sqrtPriceX96, liquidity, tick
|
||||||
|
data := make([]byte, 32*5) // 5 * 32 bytes
|
||||||
|
|
||||||
|
// int256 amount0
|
||||||
|
if tt.amount0.Sign() < 0 {
|
||||||
|
// Two's complement for negative numbers
|
||||||
|
negAmount0 := new(big.Int).Neg(tt.amount0)
|
||||||
|
negAmount0.Sub(new(big.Int).Lsh(big.NewInt(1), 256), negAmount0)
|
||||||
|
negAmount0.FillBytes(data[0:32])
|
||||||
|
} else {
|
||||||
|
tt.amount0.FillBytes(data[0:32])
|
||||||
|
}
|
||||||
|
|
||||||
|
// int256 amount1
|
||||||
|
if tt.amount1.Sign() < 0 {
|
||||||
|
// Two's complement for negative numbers
|
||||||
|
negAmount1 := new(big.Int).Neg(tt.amount1)
|
||||||
|
negAmount1.Sub(new(big.Int).Lsh(big.NewInt(1), 256), negAmount1)
|
||||||
|
negAmount1.FillBytes(data[32:64])
|
||||||
|
} else {
|
||||||
|
tt.amount1.FillBytes(data[32:64])
|
||||||
|
}
|
||||||
|
|
||||||
|
// uint160 sqrtPriceX96
|
||||||
|
tt.sqrtPriceX96.FillBytes(data[64:96])
|
||||||
|
|
||||||
|
// uint128 liquidity
|
||||||
|
tt.liquidity.FillBytes(data[96:128])
|
||||||
|
|
||||||
|
// int24 tick
|
||||||
|
tickBig := big.NewInt(int64(tt.tick))
|
||||||
|
if tt.tick < 0 {
|
||||||
|
// Two's complement for 24-bit negative number
|
||||||
|
negTick := new(big.Int).Neg(tickBig)
|
||||||
|
negTick.Sub(new(big.Int).Lsh(big.NewInt(1), 24), negTick)
|
||||||
|
tickBytes := negTick.Bytes()
|
||||||
|
// Pad to 32 bytes
|
||||||
|
copy(data[128+(32-len(tickBytes)):], tickBytes)
|
||||||
|
} else {
|
||||||
|
tickBig.FillBytes(data[128:160])
|
||||||
|
}
|
||||||
|
|
||||||
|
log := types.Log{
|
||||||
|
Address: poolAddress,
|
||||||
|
Topics: []common.Hash{
|
||||||
|
SwapV3EventSignature,
|
||||||
|
common.BytesToHash(sender.Bytes()),
|
||||||
|
common.BytesToHash(recipient.Bytes()),
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
BlockNumber: 1000,
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
event, err := parser.ParseLog(ctx, log, tx)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ParseLog() expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseLog() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == nil {
|
||||||
|
t.Fatal("ParseLog() returned nil event")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify event fields
|
||||||
|
if event.TxHash != tx.Hash() {
|
||||||
|
t.Errorf("TxHash = %v, want %v", event.TxHash, tx.Hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Protocol != mevtypes.ProtocolUniswapV3 {
|
||||||
|
t.Errorf("Protocol = %v, want %v", event.Protocol, mevtypes.ProtocolUniswapV3)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Amount0In.Cmp(tt.wantAmount0In) != 0 {
|
||||||
|
t.Errorf("Amount0In = %v, want %v", event.Amount0In, tt.wantAmount0In)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Amount1In.Cmp(tt.wantAmount1In) != 0 {
|
||||||
|
t.Errorf("Amount1In = %v, want %v", event.Amount1In, tt.wantAmount1In)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Amount0Out.Cmp(tt.wantAmount0Out) != 0 {
|
||||||
|
t.Errorf("Amount0Out = %v, want %v", event.Amount0Out, tt.wantAmount0Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Amount1Out.Cmp(tt.wantAmount1Out) != 0 {
|
||||||
|
t.Errorf("Amount1Out = %v, want %v", event.Amount1Out, tt.wantAmount1Out)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.SqrtPriceX96.Cmp(tt.sqrtPriceX96) != 0 {
|
||||||
|
t.Errorf("SqrtPriceX96 = %v, want %v", event.SqrtPriceX96, tt.sqrtPriceX96)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Liquidity.Cmp(tt.liquidity) != 0 {
|
||||||
|
t.Errorf("Liquidity = %v, want %v", event.Liquidity, tt.liquidity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Tick == nil {
|
||||||
|
t.Error("Tick is nil")
|
||||||
|
} else if *event.Tick != tt.tick {
|
||||||
|
t.Errorf("Tick = %v, want %v", *event.Tick, tt.tick)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUniswapV3Parser_ParseReceipt(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create pool cache and add test pool
|
||||||
|
poolCache := cache.NewPoolCache()
|
||||||
|
poolAddress := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||||
|
token0 := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||||
|
token1 := common.HexToAddress("0x3333333333333333333333333333333333333333")
|
||||||
|
|
||||||
|
testPool := &mevtypes.PoolInfo{
|
||||||
|
Address: poolAddress,
|
||||||
|
Protocol: mevtypes.ProtocolUniswapV3,
|
||||||
|
Token0: token0,
|
||||||
|
Token1: token1,
|
||||||
|
Token0Decimals: 18,
|
||||||
|
Token1Decimals: 6,
|
||||||
|
Reserve0: big.NewInt(1000000),
|
||||||
|
Reserve1: big.NewInt(500000),
|
||||||
|
Fee: 500,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := poolCache.Add(ctx, testPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to add test pool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewUniswapV3Parser(poolCache, &mockLogger{})
|
||||||
|
|
||||||
|
// Create test transaction
|
||||||
|
tx := types.NewTransaction(
|
||||||
|
0,
|
||||||
|
poolAddress,
|
||||||
|
big.NewInt(0),
|
||||||
|
0,
|
||||||
|
big.NewInt(0),
|
||||||
|
[]byte{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode minimal valid event data
|
||||||
|
amount0 := big.NewInt(-1000000000000000000) // -1 token0
|
||||||
|
amount1 := big.NewInt(500000) // +0.5 token1
|
||||||
|
sqrtPriceX96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
||||||
|
liquidity := big.NewInt(1000000)
|
||||||
|
tick := big.NewInt(100)
|
||||||
|
|
||||||
|
data := make([]byte, 32*5)
|
||||||
|
// Negative amount0 (two's complement)
|
||||||
|
negAmount0 := new(big.Int).Neg(amount0)
|
||||||
|
negAmount0.Sub(new(big.Int).Lsh(big.NewInt(1), 256), negAmount0)
|
||||||
|
negAmount0.FillBytes(data[0:32])
|
||||||
|
amount1.FillBytes(data[32:64])
|
||||||
|
sqrtPriceX96.FillBytes(data[64:96])
|
||||||
|
liquidity.FillBytes(data[96:128])
|
||||||
|
tick.FillBytes(data[128:160])
|
||||||
|
|
||||||
|
sender := common.HexToAddress("0x4444444444444444444444444444444444444444")
|
||||||
|
recipient := common.HexToAddress("0x5555555555555555555555555555555555555555")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
receipt *types.Receipt
|
||||||
|
wantCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "receipt with single V3 swap event",
|
||||||
|
receipt: &types.Receipt{
|
||||||
|
Logs: []*types.Log{
|
||||||
|
{
|
||||||
|
Address: poolAddress,
|
||||||
|
Topics: []common.Hash{
|
||||||
|
SwapV3EventSignature,
|
||||||
|
common.BytesToHash(sender.Bytes()),
|
||||||
|
common.BytesToHash(recipient.Bytes()),
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
BlockNumber: 1000,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "receipt with multiple V3 swap events",
|
||||||
|
receipt: &types.Receipt{
|
||||||
|
Logs: []*types.Log{
|
||||||
|
{
|
||||||
|
Address: poolAddress,
|
||||||
|
Topics: []common.Hash{
|
||||||
|
SwapV3EventSignature,
|
||||||
|
common.BytesToHash(sender.Bytes()),
|
||||||
|
common.BytesToHash(recipient.Bytes()),
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
BlockNumber: 1000,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Address: poolAddress,
|
||||||
|
Topics: []common.Hash{
|
||||||
|
SwapV3EventSignature,
|
||||||
|
common.BytesToHash(sender.Bytes()),
|
||||||
|
common.BytesToHash(recipient.Bytes()),
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
BlockNumber: 1000,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "receipt with mixed V2 and V3 events",
|
||||||
|
receipt: &types.Receipt{
|
||||||
|
Logs: []*types.Log{
|
||||||
|
{
|
||||||
|
Address: poolAddress,
|
||||||
|
Topics: []common.Hash{
|
||||||
|
SwapV3EventSignature,
|
||||||
|
common.BytesToHash(sender.Bytes()),
|
||||||
|
common.BytesToHash(recipient.Bytes()),
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
BlockNumber: 1000,
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Address: poolAddress,
|
||||||
|
Topics: []common.Hash{
|
||||||
|
SwapEventSignature, // V2 signature
|
||||||
|
common.BytesToHash(sender.Bytes()),
|
||||||
|
common.BytesToHash(recipient.Bytes()),
|
||||||
|
},
|
||||||
|
Data: []byte{},
|
||||||
|
BlockNumber: 1000,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantCount: 1, // Only the V3 event
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty receipt",
|
||||||
|
receipt: &types.Receipt{
|
||||||
|
Logs: []*types.Log{},
|
||||||
|
},
|
||||||
|
wantCount: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
events, err := parser.ParseReceipt(ctx, tt.receipt, tx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseReceipt() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(events) != tt.wantCount {
|
||||||
|
t.Errorf("ParseReceipt() returned %d events, want %d", len(events), tt.wantCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all returned events are valid
|
||||||
|
for i, event := range events {
|
||||||
|
if event == nil {
|
||||||
|
t.Errorf("Event %d is nil", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Protocol != mevtypes.ProtocolUniswapV3 {
|
||||||
|
t.Errorf("Event %d Protocol = %v, want %v", i, event.Protocol, mevtypes.ProtocolUniswapV3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwapV3EventSignature(t *testing.T) {
|
||||||
|
// Verify the event signature is correct
|
||||||
|
expected := crypto.Keccak256Hash([]byte("Swap(address,address,int256,int256,uint160,uint128,int24)"))
|
||||||
|
|
||||||
|
if SwapV3EventSignature != expected {
|
||||||
|
t.Errorf("SwapV3EventSignature = %v, want %v", SwapV3EventSignature, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculatePriceFromSqrtPriceX96(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sqrtPriceX96 *big.Int
|
||||||
|
token0Decimals uint8
|
||||||
|
token1Decimals uint8
|
||||||
|
wantNonZero bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid sqrtPriceX96",
|
||||||
|
sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96), // Price = 1
|
||||||
|
token0Decimals: 18,
|
||||||
|
token1Decimals: 18,
|
||||||
|
wantNonZero: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil sqrtPriceX96",
|
||||||
|
sqrtPriceX96: nil,
|
||||||
|
token0Decimals: 18,
|
||||||
|
token1Decimals: 18,
|
||||||
|
wantNonZero: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero sqrtPriceX96",
|
||||||
|
sqrtPriceX96: big.NewInt(0),
|
||||||
|
token0Decimals: 18,
|
||||||
|
token1Decimals: 18,
|
||||||
|
wantNonZero: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different decimals",
|
||||||
|
sqrtPriceX96: new(big.Int).Lsh(big.NewInt(1), 96),
|
||||||
|
token0Decimals: 18,
|
||||||
|
token1Decimals: 6,
|
||||||
|
wantNonZero: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
price := CalculatePriceFromSqrtPriceX96(tt.sqrtPriceX96, tt.token0Decimals, tt.token1Decimals)
|
||||||
|
|
||||||
|
if tt.wantNonZero {
|
||||||
|
if price.Sign() == 0 {
|
||||||
|
t.Error("CalculatePriceFromSqrtPriceX96() returned zero, want non-zero")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if price.Sign() != 0 {
|
||||||
|
t.Error("CalculatePriceFromSqrtPriceX96() returned non-zero, want zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,8 +87,8 @@ func (p *PoolInfo) CalculatePrice() *big.Float {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scale reserves to 18 decimals for consistent calculation
|
// Scale reserves to 18 decimals for consistent calculation
|
||||||
reserve0Scaled := scaleToDecimals(p.Reserve0, p.Token0Decimals, 18)
|
reserve0Scaled := ScaleToDecimals(p.Reserve0, p.Token0Decimals, 18)
|
||||||
reserve1Scaled := scaleToDecimals(p.Reserve1, p.Token1Decimals, 18)
|
reserve1Scaled := ScaleToDecimals(p.Reserve1, p.Token1Decimals, 18)
|
||||||
|
|
||||||
// Price = Reserve1 / Reserve0
|
// Price = Reserve1 / Reserve0
|
||||||
reserve0Float := new(big.Float).SetInt(reserve0Scaled)
|
reserve0Float := new(big.Float).SetInt(reserve0Scaled)
|
||||||
@@ -98,8 +98,8 @@ func (p *PoolInfo) CalculatePrice() *big.Float {
|
|||||||
return price
|
return price
|
||||||
}
|
}
|
||||||
|
|
||||||
// scaleToDecimals scales an amount from one decimal precision to another
|
// ScaleToDecimals scales an amount from one decimal precision to another
|
||||||
func scaleToDecimals(amount *big.Int, fromDecimals, toDecimals uint8) *big.Int {
|
func ScaleToDecimals(amount *big.Int, fromDecimals, toDecimals uint8) *big.Int {
|
||||||
if fromDecimals == toDecimals {
|
if fromDecimals == toDecimals {
|
||||||
return new(big.Int).Set(amount)
|
return new(big.Int).Set(amount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ func TestPoolInfo_CalculatePrice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_scaleToDecimals(t *testing.T) {
|
func TestScaleToDecimals(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
amount *big.Int
|
amount *big.Int
|
||||||
@@ -277,9 +277,9 @@ func Test_scaleToDecimals(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := scaleToDecimals(tt.amount, tt.fromDecimals, tt.toDecimals)
|
got := ScaleToDecimals(tt.amount, tt.fromDecimals, tt.toDecimals)
|
||||||
if got.Cmp(tt.want) != 0 {
|
if got.Cmp(tt.want) != 0 {
|
||||||
t.Errorf("scaleToDecimals() = %v, want %v", got, tt.want)
|
t.Errorf("ScaleToDecimals() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user