feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
554
orig/pkg/uniswap/contracts.go
Normal file
554
orig/pkg/uniswap/contracts.go
Normal file
@@ -0,0 +1,554 @@
|
||||
package uniswap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// UniswapV3Pool represents a Uniswap V3 pool contract interface
|
||||
type UniswapV3Pool struct {
|
||||
address common.Address
|
||||
client *ethclient.Client
|
||||
abi abi.ABI
|
||||
}
|
||||
|
||||
// PoolState represents the current state of a Uniswap V3 pool
|
||||
type PoolState struct {
|
||||
SqrtPriceX96 *uint256.Int
|
||||
Tick int
|
||||
Liquidity *uint256.Int
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
Fee int64
|
||||
}
|
||||
|
||||
// Uniswap V3 Pool ABI (only the functions we need)
|
||||
const UniswapV3PoolABI = `[
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "slot0",
|
||||
"outputs": [
|
||||
{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"},
|
||||
{"internalType": "int24", "name": "tick", "type": "int24"},
|
||||
{"internalType": "uint16", "name": "observationIndex", "type": "uint16"},
|
||||
{"internalType": "uint16", "name": "observationCardinality", "type": "uint16"},
|
||||
{"internalType": "uint16", "name": "observationCardinalityNext", "type": "uint16"},
|
||||
{"internalType": "uint8", "name": "feeProtocol", "type": "uint8"},
|
||||
{"internalType": "bool", "name": "unlocked", "type": "bool"}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "liquidity",
|
||||
"outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "token0",
|
||||
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "token1",
|
||||
"outputs": [{"internalType": "address", "name": "", "type": "address"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "fee",
|
||||
"outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]`
|
||||
|
||||
// NewUniswapV3Pool creates a new Uniswap V3 pool interface
|
||||
func NewUniswapV3Pool(address common.Address, client *ethclient.Client) *UniswapV3Pool {
|
||||
// Parse the ABI
|
||||
parsedABI, err := abi.JSON(strings.NewReader(UniswapV3PoolABI))
|
||||
if err != nil {
|
||||
// If ABI parsing fails, continue with empty ABI (fallback mode)
|
||||
parsedABI = abi.ABI{}
|
||||
}
|
||||
|
||||
return &UniswapV3Pool{
|
||||
address: address,
|
||||
client: client,
|
||||
abi: parsedABI,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPoolState fetches the current state of a Uniswap V3 pool
|
||||
func (p *UniswapV3Pool) GetPoolState(ctx context.Context) (*PoolState, error) {
|
||||
// ENHANCED: Use pool detector to verify this is actually a V3 pool before attempting slot0()
|
||||
detector := NewPoolDetector(p.client)
|
||||
poolVersion, err := detector.DetectPoolVersion(ctx, p.address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect pool version for %s: %w", p.address.Hex(), err)
|
||||
}
|
||||
|
||||
// If not a V3 pool, return a descriptive error
|
||||
if poolVersion != PoolVersionV3 {
|
||||
return nil, fmt.Errorf("pool %s is %s, not Uniswap V3 (cannot call slot0)", p.address.Hex(), poolVersion.String())
|
||||
}
|
||||
|
||||
// Call slot0() to get sqrtPriceX96, tick, and other slot0 data
|
||||
slot0Data, err := p.callSlot0(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call slot0: %w", err)
|
||||
}
|
||||
|
||||
// Call liquidity() to get current liquidity
|
||||
liquidity, err := p.callLiquidity(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call liquidity: %w", err)
|
||||
}
|
||||
|
||||
// Call token0() and token1() to get token addresses
|
||||
token0, err := p.callToken0(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call token0: %w", err)
|
||||
}
|
||||
|
||||
token1, err := p.callToken1(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call token1: %w", err)
|
||||
}
|
||||
|
||||
// Call fee() to get fee tier
|
||||
fee, err := p.callFee(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call fee: %w", err)
|
||||
}
|
||||
|
||||
return &PoolState{
|
||||
SqrtPriceX96: slot0Data.SqrtPriceX96,
|
||||
Tick: slot0Data.Tick,
|
||||
Liquidity: liquidity,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Fee: fee,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Slot0Data represents the data returned by slot0()
|
||||
type Slot0Data struct {
|
||||
SqrtPriceX96 *uint256.Int
|
||||
Tick int
|
||||
ObservationIndex int
|
||||
ObservationCardinality int
|
||||
ObservationCardinalityNext int
|
||||
FeeProtocol int
|
||||
Unlocked bool
|
||||
}
|
||||
|
||||
// callSlot0 calls the slot0() function on the pool contract
|
||||
func (p *UniswapV3Pool) callSlot0(ctx context.Context) (*Slot0Data, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack("slot0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack slot0 call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call slot0: %w", err)
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Check for empty response (indicates V2 pool or invalid contract)
|
||||
if len(result) == 0 {
|
||||
return nil, fmt.Errorf("empty response from slot0 call - pool %s may be V2 (no slot0 function) or invalid contract", p.address.Hex())
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Use Unpack() method which returns values directly, not UnpackIntoInterface
|
||||
unpacked, err := p.abi.Unpack("slot0", result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack slot0 result (got %d bytes): %w", len(result), err)
|
||||
}
|
||||
|
||||
// Ensure we have the expected number of return values
|
||||
if len(unpacked) < 7 {
|
||||
return nil, fmt.Errorf("unexpected number of return values from slot0: got %d, expected 7 (pool may not be UniswapV3)", len(unpacked))
|
||||
}
|
||||
|
||||
// Convert the unpacked values
|
||||
sqrtPriceX96, ok := unpacked[0].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert sqrtPriceX96 to *big.Int")
|
||||
}
|
||||
|
||||
tick, ok := unpacked[1].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert tick to *big.Int")
|
||||
}
|
||||
|
||||
observationIndex, ok := unpacked[2].(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert observationIndex to uint16")
|
||||
}
|
||||
|
||||
observationCardinality, ok := unpacked[3].(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert observationCardinality to uint16")
|
||||
}
|
||||
|
||||
observationCardinalityNext, ok := unpacked[4].(uint16)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert observationCardinalityNext to uint16")
|
||||
}
|
||||
|
||||
feeProtocol, ok := unpacked[5].(uint8)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert feeProtocol to uint8")
|
||||
}
|
||||
|
||||
unlocked, ok := unpacked[6].(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert unlocked to bool")
|
||||
}
|
||||
|
||||
return &Slot0Data{
|
||||
SqrtPriceX96: uint256.MustFromBig(sqrtPriceX96),
|
||||
Tick: int(tick.Int64()),
|
||||
ObservationIndex: int(observationIndex),
|
||||
ObservationCardinality: int(observationCardinality),
|
||||
ObservationCardinalityNext: int(observationCardinalityNext),
|
||||
FeeProtocol: int(feeProtocol),
|
||||
Unlocked: unlocked,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// callLiquidity calls the liquidity() function on the pool contract
|
||||
func (p *UniswapV3Pool) callLiquidity(ctx context.Context) (*uint256.Int, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack("liquidity")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack liquidity call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call liquidity: %w", err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var liquidity *big.Int
|
||||
err = p.abi.UnpackIntoInterface(&liquidity, "liquidity", result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack liquidity result: %w", err)
|
||||
}
|
||||
|
||||
return uint256.MustFromBig(liquidity), nil
|
||||
}
|
||||
|
||||
// callToken0 calls the token0() function on the pool contract
|
||||
func (p *UniswapV3Pool) callToken0(ctx context.Context) (common.Address, error) {
|
||||
return p.callToken(ctx, "token0")
|
||||
}
|
||||
|
||||
// callToken1 calls the token1() function on the pool contract
|
||||
func (p *UniswapV3Pool) callToken1(ctx context.Context) (common.Address, error) {
|
||||
return p.callToken(ctx, "token1")
|
||||
}
|
||||
|
||||
// callToken is a generic function to call token0() or token1() functions on the pool contract
|
||||
func (p *UniswapV3Pool) callToken(ctx context.Context, tokenFunc string) (common.Address, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack(tokenFunc)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to pack %s call: %w", tokenFunc, err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to call %s: %w", tokenFunc, err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var token common.Address
|
||||
err = p.abi.UnpackIntoInterface(&token, tokenFunc, result)
|
||||
if err != nil {
|
||||
return common.Address{}, fmt.Errorf("failed to unpack %s result: %w", tokenFunc, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// callFee calls the fee() function on the pool contract
|
||||
func (p *UniswapV3Pool) callFee(ctx context.Context) (int64, error) {
|
||||
// Pack the function call
|
||||
data, err := p.abi.Pack("fee")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to pack fee call: %w", err)
|
||||
}
|
||||
|
||||
// Make the contract call
|
||||
msg := ethereum.CallMsg{
|
||||
To: &p.address,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, msg, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to call fee: %w", err)
|
||||
}
|
||||
|
||||
// Unpack the result
|
||||
var fee *big.Int
|
||||
err = p.abi.UnpackIntoInterface(&fee, "fee", result)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to unpack fee result: %w", err)
|
||||
}
|
||||
|
||||
return fee.Int64(), nil
|
||||
}
|
||||
|
||||
// CalculatePoolAddress calculates the deterministic address of a Uniswap V3 pool
|
||||
func CalculatePoolAddress(factory common.Address, token0, token1 common.Address, fee int64) common.Address {
|
||||
// This implements the CREATE2 address calculation for Uniswap V3 pools
|
||||
// Using the correct salt and init code hash for Uniswap V3
|
||||
|
||||
// Correct Uniswap V3 pool init code hash
|
||||
initCodeHash := common.HexToHash("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54")
|
||||
|
||||
// Encode the pool parameters for the salt
|
||||
encoded := make([]byte, 0, 64)
|
||||
encoded = append(encoded, token0.Bytes()...)
|
||||
encoded = append(encoded, token1.Bytes()...)
|
||||
encoded = append(encoded, common.BigToHash(big.NewInt(fee)).Bytes()...)
|
||||
|
||||
// Calculate the salt
|
||||
salt := crypto.Keccak256Hash(encoded)
|
||||
|
||||
// Calculate CREATE2 address
|
||||
addr := crypto.CreateAddress2(factory, salt, initCodeHash.Bytes())
|
||||
return addr
|
||||
}
|
||||
|
||||
// IsValidPool checks if an address is a valid pool by checking for code existence
|
||||
// This is a simplified version to break import cycle - use PoolValidator for comprehensive validation
|
||||
func IsValidPool(ctx context.Context, client *ethclient.Client, address common.Address) bool {
|
||||
// Check if address has code (basic contract existence check)
|
||||
code, err := client.CodeAt(ctx, address, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Must have contract code
|
||||
if len(code) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional basic check: ensure it's not zero address
|
||||
return address != common.HexToAddress("0x0000000000000000000000000000000000000000")
|
||||
}
|
||||
|
||||
// ParseABI parses an ABI JSON string and returns the parsed ABI
|
||||
func ParseABI(abiJSON string) (abi.ABI, error) {
|
||||
return abi.JSON(strings.NewReader(abiJSON))
|
||||
}
|
||||
|
||||
// UniswapV3Pricing provides Uniswap V3 pricing calculations
|
||||
type UniswapV3Pricing struct {
|
||||
client *ethclient.Client
|
||||
}
|
||||
|
||||
// NewUniswapV3Pricing creates a new Uniswap V3 pricing calculator
|
||||
func NewUniswapV3Pricing(client *ethclient.Client) *UniswapV3Pricing {
|
||||
return &UniswapV3Pricing{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPrice calculates the price for a token pair by querying Uniswap V3 pools
|
||||
func (p *UniswapV3Pricing) GetPrice(ctx context.Context, token0, token1 common.Address) (*big.Int, error) {
|
||||
// This is a simplified implementation that queries a common WETH/USDC pool
|
||||
// In production, you would:
|
||||
// 1. Discover pools for the token pair
|
||||
// 2. Query multiple pools to get the best price
|
||||
// 3. Handle different fee tiers
|
||||
|
||||
// For demonstration, we'll use a common pool (WETH/USDC 0.05% fee)
|
||||
// In practice, you would dynamically discover pools for the token pair
|
||||
poolAddress := common.HexToAddress("0xC6962004f452bE9203591991D15f6b388e09E8D0") // WETH/USDC 0.05% pool on Arbitrum
|
||||
|
||||
// Create pool interface
|
||||
pool := NewUniswapV3Pool(poolAddress, p.client)
|
||||
|
||||
// Get pool state
|
||||
poolState, err := pool.GetPoolState(ctx)
|
||||
if err != nil {
|
||||
// Fallback to realistic mock data with per-pool variation
|
||||
// This simulates what you'd get from a real pool but with deterministic variation
|
||||
|
||||
// Create variation based on token addresses to make different token pairs have different prices
|
||||
token0Bytes := token0.Bytes()
|
||||
token1Bytes := token1.Bytes()
|
||||
|
||||
// Simple hash-based variation
|
||||
variation := int64(token0Bytes[19]) - int64(token1Bytes[19])
|
||||
|
||||
// Base price (in wei, representing price with 18 decimals)
|
||||
basePriceStr := "2000000000000000000000" // 2000 USDC per WETH (2000 * 10^18)
|
||||
basePrice, ok := new(big.Int).SetString(basePriceStr, 10)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse base price")
|
||||
}
|
||||
|
||||
// Apply variation (-50% to +50%)
|
||||
variationBig := big.NewInt(variation)
|
||||
hundred := big.NewInt(100)
|
||||
priceVariation := new(big.Int).Mul(basePrice, variationBig)
|
||||
priceVariation.Div(priceVariation, hundred)
|
||||
finalPrice := new(big.Int).Add(basePrice, priceVariation)
|
||||
|
||||
// Ensure price is positive
|
||||
if finalPrice.Sign() <= 0 {
|
||||
finalPrice = basePrice
|
||||
}
|
||||
|
||||
return finalPrice, nil
|
||||
}
|
||||
|
||||
// Convert sqrtPriceX96 to actual price
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
sqrtPriceX96 := poolState.SqrtPriceX96.ToBig()
|
||||
|
||||
// Calculate sqrtPriceX96^2
|
||||
sqrtPriceSquared := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
||||
|
||||
// Divide by 2^192 (which is (2^96)^2)
|
||||
q192 := new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
price := new(big.Int).Div(sqrtPriceSquared, q192)
|
||||
|
||||
return price, nil
|
||||
}
|
||||
|
||||
// SqrtPriceX96ToPrice converts sqrtPriceX96 to price
|
||||
func (p *UniswapV3Pricing) SqrtPriceX96ToPrice(sqrtPriceX96 *big.Int) *big.Int {
|
||||
// Convert sqrtPriceX96 to actual price
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
|
||||
if sqrtPriceX96 == nil {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
// Calculate sqrtPriceX96^2
|
||||
sqrtPriceSquared := new(big.Int).Mul(sqrtPriceX96, sqrtPriceX96)
|
||||
|
||||
// Divide by 2^192 (which is (2^96)^2)
|
||||
q192 := new(big.Int).Exp(big.NewInt(2), big.NewInt(192), nil)
|
||||
price := new(big.Int).Div(sqrtPriceSquared, q192)
|
||||
|
||||
return price
|
||||
}
|
||||
|
||||
// CalculateAmountOut calculates output amount using proper Uniswap V3 concentrated liquidity math
|
||||
func (p *UniswapV3Pricing) CalculateAmountOut(amountIn, sqrtPriceX96, liquidity *big.Int) (*big.Int, error) {
|
||||
if amountIn == nil || sqrtPriceX96 == nil || liquidity == nil {
|
||||
return nil, fmt.Errorf("input parameters cannot be nil")
|
||||
}
|
||||
|
||||
if amountIn.Sign() <= 0 || sqrtPriceX96.Sign() <= 0 || liquidity.Sign() <= 0 {
|
||||
return nil, fmt.Errorf("input parameters must be positive")
|
||||
}
|
||||
|
||||
// Implement proper Uniswap V3 concentrated liquidity calculation
|
||||
// Based on the formula: Δy = L * (√P₁ - √P₀) where L is liquidity
|
||||
// And the price movement: √P₁ = √P₀ + Δx / L
|
||||
|
||||
// For token0 -> token1 swap:
|
||||
// 1. Calculate new sqrt price after swap
|
||||
// 2. Calculate output amount based on liquidity and price change
|
||||
|
||||
// Calculate Δ(sqrt(P)) based on input amount and liquidity
|
||||
// For exact input: Δ(1/√P) = Δx / L
|
||||
// So: 1/√P₁ = 1/√P₀ + Δx / L
|
||||
// Therefore: √P₁ = √P₀ / (1 + Δx * √P₀ / L)
|
||||
|
||||
// Calculate the new sqrt price after the swap
|
||||
numerator := new(big.Int).Mul(amountIn, sqrtPriceX96)
|
||||
denominator := new(big.Int).Add(liquidity, numerator)
|
||||
|
||||
// Check for overflow/underflow
|
||||
if denominator.Sign() <= 0 {
|
||||
return nil, fmt.Errorf("invalid calculation: denominator non-positive")
|
||||
}
|
||||
|
||||
sqrtPriceNext := new(big.Int).Div(new(big.Int).Mul(liquidity, sqrtPriceX96), denominator)
|
||||
|
||||
// Calculate the output amount: Δy = L * (√P₀ - √P₁)
|
||||
priceDiff := new(big.Int).Sub(sqrtPriceX96, sqrtPriceNext)
|
||||
amountOut := new(big.Int).Mul(liquidity, priceDiff)
|
||||
|
||||
// Adjust for Q96 scaling: divide by 2^96
|
||||
q96 := new(big.Int).Lsh(big.NewInt(1), 96)
|
||||
amountOut.Div(amountOut, q96)
|
||||
|
||||
// Apply trading fee (typically 0.3% = 3000 basis points for most pools)
|
||||
// Fee is taken from input, so output is calculated on (amountIn - fee)
|
||||
fee := big.NewInt(3000) // 0.3% in basis points
|
||||
feeAmount := new(big.Int).Mul(amountOut, fee)
|
||||
feeAmount.Div(feeAmount, big.NewInt(1000000)) // Divide by 1M to get basis points
|
||||
amountOut.Sub(amountOut, feeAmount)
|
||||
|
||||
// Additional slippage protection for large trades
|
||||
// If trade is > 1% of liquidity, apply additional slippage
|
||||
tradeSize := new(big.Int).Mul(amountIn, big.NewInt(100))
|
||||
if tradeSize.Cmp(liquidity) > 0 {
|
||||
// Large trade - apply additional slippage of 0.1% per 1% of liquidity
|
||||
liquidityRatio := new(big.Int).Div(tradeSize, liquidity)
|
||||
additionalSlippage := new(big.Int).Mul(amountOut, liquidityRatio)
|
||||
additionalSlippage.Div(additionalSlippage, big.NewInt(10000)) // 0.01% base slippage
|
||||
amountOut.Sub(amountOut, additionalSlippage)
|
||||
}
|
||||
|
||||
// Ensure result is not negative and is reasonable compared to input
|
||||
if amountOut.Sign() < 0 {
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
|
||||
// Additional validation: output amount should not be significantly larger than input
|
||||
// This prevents unrealistic values due to liquidity/price calculation errors
|
||||
maxReasonableOutput := new(big.Int).Mul(amountIn, big.NewInt(2)) // 2x input as max reasonable output
|
||||
if amountOut.Cmp(maxReasonableOutput) > 0 {
|
||||
return nil, fmt.Errorf("calculated output amount is unreasonably large: %s vs input %s",
|
||||
amountOut.String(), amountIn.String())
|
||||
}
|
||||
|
||||
return amountOut, nil
|
||||
}
|
||||
Reference in New Issue
Block a user