package dex 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/core/types" "github.com/ethereum/go-ethereum/ethclient" ) // CurveDecoder implements DEXDecoder for Curve Finance (StableSwap) type CurveDecoder struct { *BaseDecoder poolABI abi.ABI } // Curve StableSwap Pool ABI (minimal) const curvePoolABI = `[ { "name": "get_dy", "outputs": [{"type": "uint256", "name": ""}], "inputs": [ {"type": "int128", "name": "i"}, {"type": "int128", "name": "j"}, {"type": "uint256", "name": "dx"} ], "stateMutability": "view", "type": "function" }, { "name": "exchange", "outputs": [{"type": "uint256", "name": ""}], "inputs": [ {"type": "int128", "name": "i"}, {"type": "int128", "name": "j"}, {"type": "uint256", "name": "dx"}, {"type": "uint256", "name": "min_dy"} ], "stateMutability": "payable", "type": "function" }, { "name": "coins", "outputs": [{"type": "address", "name": ""}], "inputs": [{"type": "uint256", "name": "arg0"}], "stateMutability": "view", "type": "function" }, { "name": "balances", "outputs": [{"type": "uint256", "name": ""}], "inputs": [{"type": "uint256", "name": "arg0"}], "stateMutability": "view", "type": "function" }, { "name": "A", "outputs": [{"type": "uint256", "name": ""}], "inputs": [], "stateMutability": "view", "type": "function" }, { "name": "fee", "outputs": [{"type": "uint256", "name": ""}], "inputs": [], "stateMutability": "view", "type": "function" } ]` // NewCurveDecoder creates a new Curve decoder func NewCurveDecoder(client *ethclient.Client) *CurveDecoder { poolABI, _ := abi.JSON(strings.NewReader(curvePoolABI)) return &CurveDecoder{ BaseDecoder: NewBaseDecoder(ProtocolCurve, client), poolABI: poolABI, } } // DecodeSwap decodes a Curve swap transaction func (d *CurveDecoder) DecodeSwap(tx *types.Transaction) (*SwapInfo, error) { data := tx.Data() if len(data) < 4 { return nil, fmt.Errorf("transaction data too short") } method, err := d.poolABI.MethodById(data[:4]) if err != nil { return nil, fmt.Errorf("failed to get method: %w", err) } if method.Name != "exchange" { return nil, fmt.Errorf("unsupported method: %s", method.Name) } params := make(map[string]interface{}) if err := method.Inputs.UnpackIntoMap(params, data[4:]); err != nil { return nil, fmt.Errorf("failed to unpack params: %w", err) } // Curve uses indices for tokens, need to fetch actual addresses // This is a simplified version - production would cache token addresses poolAddress := *tx.To() return &SwapInfo{ Protocol: ProtocolCurve, PoolAddress: poolAddress, AmountIn: params["dx"].(*big.Int), AmountOut: params["min_dy"].(*big.Int), Fee: big.NewInt(4), // 0.04% typical Curve fee }, nil } // GetPoolReserves fetches current pool reserves for Curve func (d *CurveDecoder) GetPoolReserves(ctx context.Context, client *ethclient.Client, poolAddress common.Address) (*PoolReserves, error) { // Get amplification coefficient A aData, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: d.poolABI.Methods["A"].ID, }, nil) if err != nil { return nil, fmt.Errorf("failed to get A: %w", err) } amplificationCoeff := new(big.Int).SetBytes(aData) // Get fee feeData, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: d.poolABI.Methods["fee"].ID, }, nil) if err != nil { return nil, fmt.Errorf("failed to get fee: %w", err) } fee := new(big.Int).SetBytes(feeData) // Get token0 (index 0) token0Calldata, err := d.poolABI.Pack("coins", big.NewInt(0)) if err != nil { return nil, fmt.Errorf("failed to pack coins(0): %w", err) } token0Data, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: token0Calldata, }, nil) if err != nil { return nil, fmt.Errorf("failed to get token0: %w", err) } token0 := common.BytesToAddress(token0Data) // Get token1 (index 1) token1Calldata, err := d.poolABI.Pack("coins", big.NewInt(1)) if err != nil { return nil, fmt.Errorf("failed to pack coins(1): %w", err) } token1Data, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: token1Calldata, }, nil) if err != nil { return nil, fmt.Errorf("failed to get token1: %w", err) } token1 := common.BytesToAddress(token1Data) // Get balance0 balance0Calldata, err := d.poolABI.Pack("balances", big.NewInt(0)) if err != nil { return nil, fmt.Errorf("failed to pack balances(0): %w", err) } balance0Data, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: balance0Calldata, }, nil) if err != nil { return nil, fmt.Errorf("failed to get balance0: %w", err) } reserve0 := new(big.Int).SetBytes(balance0Data) // Get balance1 balance1Calldata, err := d.poolABI.Pack("balances", big.NewInt(1)) if err != nil { return nil, fmt.Errorf("failed to pack balances(1): %w", err) } balance1Data, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: balance1Calldata, }, nil) if err != nil { return nil, fmt.Errorf("failed to get balance1: %w", err) } reserve1 := new(big.Int).SetBytes(balance1Data) return &PoolReserves{ Token0: token0, Token1: token1, Reserve0: reserve0, Reserve1: reserve1, Protocol: ProtocolCurve, PoolAddress: poolAddress, Fee: fee, A: amplificationCoeff, }, nil } // CalculateOutput calculates expected output for Curve StableSwap func (d *CurveDecoder) CalculateOutput(amountIn *big.Int, reserves *PoolReserves, tokenIn common.Address) (*big.Int, error) { if amountIn == nil || amountIn.Sign() <= 0 { return nil, fmt.Errorf("invalid amountIn") } if reserves.A == nil { return nil, fmt.Errorf("missing amplification coefficient A") } var x, y *big.Int // x = balance of input token, y = balance of output token if tokenIn == reserves.Token0 { x = reserves.Reserve0 y = reserves.Reserve1 } else if tokenIn == reserves.Token1 { x = reserves.Reserve1 y = reserves.Reserve0 } else { return nil, fmt.Errorf("tokenIn not in pool") } if x.Sign() == 0 || y.Sign() == 0 { return nil, fmt.Errorf("insufficient liquidity") } // Simplified StableSwap calculation // Real implementation: y_new = get_y(A, x + dx, D) // This is an approximation for demonstration // For stable pairs, use near 1:1 pricing with low slippage amountOut := new(big.Int).Set(amountIn) // Apply fee (0.04% = 9996/10000) fee := reserves.Fee if fee == nil { fee = big.NewInt(4) // 0.04% } feeBasisPoints := new(big.Int).Sub(big.NewInt(10000), fee) amountOut.Mul(amountOut, feeBasisPoints) amountOut.Div(amountOut, big.NewInt(10000)) // For production: Implement full StableSwap invariant D calculation // D = A * n^n * sum(x_i) + D = A * n^n * D + D^(n+1) / (n^n * prod(x_i)) // Then solve for y given new x return amountOut, nil } // CalculatePriceImpact calculates price impact for Curve func (d *CurveDecoder) CalculatePriceImpact(amountIn *big.Int, reserves *PoolReserves, tokenIn common.Address) (float64, error) { // Curve StableSwap has very low price impact for stable pairs // Price impact increases with distance from balance point if amountIn == nil || amountIn.Sign() <= 0 { return 0, nil } var x *big.Int if tokenIn == reserves.Token0 { x = reserves.Reserve0 } else { x = reserves.Reserve1 } if x.Sign() == 0 { return 1.0, nil } // Simple approximation: impact proportional to (amountIn / reserve)^2 // StableSwap has lower impact than constant product amountInFloat := new(big.Float).SetInt(amountIn) reserveFloat := new(big.Float).SetInt(x) ratio := new(big.Float).Quo(amountInFloat, reserveFloat) impact := new(big.Float).Mul(ratio, ratio) // Square for stable curves impact.Mul(impact, big.NewFloat(0.1)) // Scale down for StableSwap efficiency impactValue, _ := impact.Float64() return impactValue, nil } // GetQuote gets a price quote for Curve func (d *CurveDecoder) GetQuote(ctx context.Context, client *ethclient.Client, tokenIn, tokenOut common.Address, amountIn *big.Int) (*PriceQuote, error) { // TODO: Implement pool lookup via Curve registry // For now, return error return nil, fmt.Errorf("GetQuote not yet implemented for Curve") } // IsValidPool checks if a pool is a valid Curve pool func (d *CurveDecoder) IsValidPool(ctx context.Context, client *ethclient.Client, poolAddress common.Address) (bool, error) { // Try to call A() - if it succeeds, it's likely a Curve pool _, err := client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddress, Data: d.poolABI.Methods["A"].ID, }, nil) return err == nil, nil }