package oracle import ( "context" "fmt" "math/big" "sync" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/logger" "github.com/fraktal/mev-beta/pkg/uniswap" ) // PriceOracle provides real-time price data for tokens type PriceOracle struct { client *ethclient.Client logger *logger.Logger priceCache map[string]*PriceData cacheMutex sync.RWMutex cacheExpiry time.Duration updateTicker *time.Ticker stopChan chan struct{} chainlinkFeeds map[common.Address]common.Address // token -> chainlink feed uniswapPools map[string]common.Address // token pair -> pool address } // PriceData represents cached price information type PriceData struct { Price *big.Int // Price in wei (18 decimals) Timestamp time.Time Source string // "chainlink", "uniswap", "coingecko" Confidence float64 // 0.0 to 1.0 } // PriceRequest represents a price query type PriceRequest struct { TokenIn common.Address TokenOut common.Address AmountIn *big.Int Timestamp time.Time } // PriceResponse contains the price calculation result type PriceResponse struct { Price *big.Int AmountOut *big.Int SlippageBps *big.Int // basis points (1% = 100 bps) PriceImpact float64 // price impact as decimal (0.01 = 1%) Liquidity *big.Int // estimated pool liquidity Source string Timestamp time.Time Valid bool } // NewPriceOracle creates a new price oracle instance func NewPriceOracle(client *ethclient.Client, logger *logger.Logger) *PriceOracle { oracle := &PriceOracle{ client: client, logger: logger, priceCache: make(map[string]*PriceData), cacheExpiry: 30 * time.Second, // 30-second cache stopChan: make(chan struct{}), chainlinkFeeds: getChainlinkFeeds(), uniswapPools: getUniswapPools(), } // Start background price updates oracle.updateTicker = time.NewTicker(15 * time.Second) go oracle.backgroundUpdater() return oracle } // GetPrice returns the current price for a token pair func (p *PriceOracle) GetPrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { if req.TokenIn == req.TokenOut { return &PriceResponse{ Price: big.NewInt(1e18), // 1:1 ratio AmountOut: new(big.Int).Set(req.AmountIn), Source: "identity", Timestamp: time.Now(), Valid: true, }, nil } // Try multiple price sources in order of preference sources := []func(context.Context, *PriceRequest) (*PriceResponse, error){ p.getChainlinkPrice, p.getUniswapV3Price, p.getUniswapV2Price, } var lastErr error for _, getPrice := range sources { if response, err := getPrice(ctx, req); err == nil && response.Valid { p.cachePrice(req, response) return response, nil } else { lastErr = err } } // Fallback to cached price if available if cached := p.getCachedPrice(req); cached != nil { p.logger.Warn(fmt.Sprintf("Using cached price for %s/%s due to oracle failures", req.TokenIn.Hex(), req.TokenOut.Hex())) return cached, nil } return nil, fmt.Errorf("all price sources failed, last error: %w", lastErr) } // getChainlinkPrice gets price from Chainlink price feeds func (p *PriceOracle) getChainlinkPrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { feedAddr, exists := p.chainlinkFeeds[req.TokenIn] if !exists { return nil, fmt.Errorf("no chainlink feed for token %s", req.TokenIn.Hex()) } // Chainlink ABI for latestRoundData() chainlinkABI := `[{"inputs":[],"name":"latestRoundData","outputs":[{"internalType":"uint80","name":"roundId","type":"uint80"},{"internalType":"int256","name":"answer","type":"int256"},{"internalType":"uint256","name":"startedAt","type":"uint256"},{"internalType":"uint256","name":"updatedAt","type":"uint256"},{"internalType":"uint80","name":"answeredInRound","type":"uint80"}],"stateMutability":"view","type":"function"}]` contractABI, err := uniswap.ParseABI(chainlinkABI) if err != nil { return nil, fmt.Errorf("failed to parse chainlink ABI: %w", err) } // Call latestRoundData callData, err := contractABI.Pack("latestRoundData") if err != nil { return nil, fmt.Errorf("failed to pack chainlink call: %w", err) } result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &feedAddr, Data: callData, }, nil) if err != nil { return nil, fmt.Errorf("chainlink call failed: %w", err) } // Unpack result unpacked, err := contractABI.Unpack("latestRoundData", result) if err != nil { return nil, fmt.Errorf("failed to unpack chainlink result: %w", err) } if len(unpacked) < 5 { return nil, fmt.Errorf("invalid chainlink response length") } answer, ok := unpacked[1].(*big.Int) if !ok || answer.Sign() <= 0 { return nil, fmt.Errorf("invalid chainlink price: %v", unpacked[1]) } updatedAt, ok := unpacked[3].(*big.Int) if !ok { return nil, fmt.Errorf("invalid chainlink timestamp: %v", unpacked[3]) } // Check if price is stale (older than 1 hour) if time.Since(time.Unix(updatedAt.Int64(), 0)) > time.Hour { return nil, fmt.Errorf("chainlink price is stale") } // Convert amount using chainlink price // Chainlink prices are typically 8 decimals, convert to 18 priceWei := new(big.Int).Mul(answer, big.NewInt(1e10)) amountOut := new(big.Int).Mul(req.AmountIn, priceWei) amountOut.Div(amountOut, big.NewInt(1e18)) return &PriceResponse{ Price: priceWei, AmountOut: amountOut, SlippageBps: big.NewInt(0), // Chainlink has no slippage Source: "chainlink", Timestamp: time.Unix(updatedAt.Int64(), 0), Valid: true, }, nil } // getUniswapV3Price gets price from Uniswap V3 pools func (p *PriceOracle) getUniswapV3Price(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { poolKey := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex()) poolAddr, exists := p.uniswapPools[poolKey] if !exists { // Try reverse pair poolKey = fmt.Sprintf("%s-%s", req.TokenOut.Hex(), req.TokenIn.Hex()) poolAddr, exists = p.uniswapPools[poolKey] if !exists { return nil, fmt.Errorf("no uniswap v3 pool for pair %s/%s", req.TokenIn.Hex(), req.TokenOut.Hex()) } } // Get pool state poolState, err := p.getPoolState(ctx, poolAddr) if err != nil { return nil, fmt.Errorf("failed to get pool state: %w", err) } // Calculate price impact and slippage using Uniswap V3 pricing pricing := uniswap.NewUniswapV3Pricing(p.client) // Get current price from pool (returns *big.Float) currentPrice := uniswap.SqrtPriceX96ToPrice(poolState.SqrtPriceX96) // Calculate output amount using Uniswap V3 math amountOut, err := pricing.CalculateAmountOut(req.AmountIn, poolState.SqrtPriceX96, poolState.Liquidity) if err != nil { return nil, fmt.Errorf("failed to calculate amount out: %w", err) } // Convert price to big.Int for slippage calculation (multiply by 1e18 for precision) priceInt := new(big.Int) currentPrice.Mul(currentPrice, big.NewFloat(1e18)) currentPrice.Int(priceInt) // Calculate slippage in basis points slippageBps, err := p.calculateSlippage(req.AmountIn, amountOut, priceInt) if err != nil { return nil, fmt.Errorf("failed to calculate slippage: %w", err) } return &PriceResponse{ Price: priceInt, // Use converted big.Int price AmountOut: amountOut, SlippageBps: slippageBps, Source: "uniswap_v3", Timestamp: time.Now(), Valid: true, }, nil } // getUniswapV2Price gets price from Uniswap V2 style pools using constant product formula func (p *PriceOracle) getUniswapV2Price(ctx context.Context, req *PriceRequest) (*PriceResponse, error) { p.logger.Debug(fmt.Sprintf("Getting Uniswap V2 price for %s/%s", req.TokenIn.Hex(), req.TokenOut.Hex())) // Find Uniswap V2 pool for this token pair poolAddr, err := p.findUniswapV2Pool(ctx, req.TokenIn, req.TokenOut) if err != nil { return nil, fmt.Errorf("failed to find Uniswap V2 pool: %w", err) } // Get pool reserves using getReserves() function reserves, err := p.getUniswapV2Reserves(ctx, poolAddr, req.TokenIn, req.TokenOut) if err != nil { return nil, fmt.Errorf("failed to get pool reserves: %w", err) } // Calculate output amount using constant product formula: x * y = k // amountOut = (amountIn * reserveOut) / (reserveIn + amountIn) amountInWithFee := new(big.Int).Mul(req.AmountIn, big.NewInt(997)) // 0.3% fee numerator := new(big.Int).Mul(amountInWithFee, reserves.ReserveOut) denominator := new(big.Int).Add(new(big.Int).Mul(reserves.ReserveIn, big.NewInt(1000)), amountInWithFee) if denominator.Sign() == 0 { return nil, fmt.Errorf("division by zero in price calculation") } amountOut := new(big.Int).Div(numerator, denominator) // Calculate price impact priceImpact := p.calculateV2PriceImpact(req.AmountIn, reserves.ReserveIn, reserves.ReserveOut) // Calculate slippage in basis points slippageBps := new(big.Int).Mul(new(big.Int).SetInt64(int64(priceImpact*10000)), big.NewInt(1)) p.logger.Debug(fmt.Sprintf("V2 price calculation: input=%s, output=%s, impact=%.4f%%", req.AmountIn.String(), amountOut.String(), priceImpact*100)) return &PriceResponse{ AmountOut: amountOut, SlippageBps: slippageBps, PriceImpact: priceImpact, Source: "uniswap_v2", Timestamp: time.Now(), Valid: true, }, nil } // V2Reserves represents the reserves in a Uniswap V2 pool type V2Reserves struct { ReserveIn *big.Int ReserveOut *big.Int BlockTimestamp uint32 } // findUniswapV2Pool finds the Uniswap V2 pool address for a token pair func (p *PriceOracle) findUniswapV2Pool(ctx context.Context, token0, token1 common.Address) (common.Address, error) { // Uniswap V2 Factory address on Arbitrum factoryAddr := common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9") // Sort tokens to match Uniswap V2 convention tokenA, tokenB := token0, token1 if token0.Big().Cmp(token1.Big()) > 0 { tokenA, tokenB = token1, token0 } // Calculate pool address using CREATE2 formula // address = keccak256(abi.encodePacked(hex"ff", factory, salt, initCodeHash))[12:] // where salt = keccak256(abi.encodePacked(token0, token1)) // Create salt from sorted token addresses salt := crypto.Keccak256Hash(append(tokenA.Bytes(), tokenB.Bytes()...)) // Uniswap V2 init code hash initCodeHash := common.HexToHash("0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f") // CREATE2 calculation create2Input := append([]byte{0xff}, factoryAddr.Bytes()...) create2Input = append(create2Input, salt.Bytes()...) create2Input = append(create2Input, initCodeHash.Bytes()...) poolHash := crypto.Keccak256Hash(create2Input) poolAddr := common.BytesToAddress(poolHash[12:]) // Verify pool exists by checking if it has code code, err := p.client.CodeAt(ctx, poolAddr, nil) if err != nil || len(code) == 0 { return common.Address{}, fmt.Errorf("pool does not exist for pair %s/%s", token0.Hex(), token1.Hex()) } return poolAddr, nil } // getUniswapV2Reserves gets the reserves from a Uniswap V2 pool func (p *PriceOracle) getUniswapV2Reserves(ctx context.Context, poolAddr common.Address, tokenIn, tokenOut common.Address) (*V2Reserves, error) { // Uniswap V2 Pair ABI for getReserves function pairABI := `[{"constant":true,"inputs":[],"name":"getReserves","outputs":[{"internalType":"uint112","name":"_reserve0","type":"uint112"},{"internalType":"uint112","name":"_reserve1","type":"uint112"},{"internalType":"uint32","name":"_blockTimestampLast","type":"uint32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]` contractABI, err := uniswap.ParseABI(pairABI) if err != nil { return nil, fmt.Errorf("failed to parse pair ABI: %w", err) } // Get getReserves data reservesData, err := contractABI.Pack("getReserves") if err != nil { return nil, fmt.Errorf("failed to pack getReserves call: %w", err) } reservesResult, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: reservesData, }, nil) if err != nil { return nil, fmt.Errorf("getReserves call failed: %w", err) } reservesUnpacked, err := contractABI.Unpack("getReserves", reservesResult) if err != nil { return nil, fmt.Errorf("failed to unpack reserves: %w", err) } reserve0 := reservesUnpacked[0].(*big.Int) reserve1 := reservesUnpacked[1].(*big.Int) blockTimestamp := reservesUnpacked[2].(uint32) // Get token0 to determine reserve order token0Data, err := contractABI.Pack("token0") if err != nil { return nil, fmt.Errorf("failed to pack token0 call: %w", err) } token0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: token0Data, }, nil) if err != nil { return nil, fmt.Errorf("token0 call failed: %w", err) } token0Unpacked, err := contractABI.Unpack("token0", token0Result) if err != nil { return nil, fmt.Errorf("failed to unpack token0: %w", err) } token0Addr := token0Unpacked[0].(common.Address) // Determine which reserve corresponds to tokenIn and tokenOut var reserveIn, reserveOut *big.Int if tokenIn == token0Addr { reserveIn, reserveOut = reserve0, reserve1 } else { reserveIn, reserveOut = reserve1, reserve0 } return &V2Reserves{ ReserveIn: reserveIn, ReserveOut: reserveOut, BlockTimestamp: blockTimestamp, }, nil } // calculateV2PriceImpact calculates the price impact for a Uniswap V2 trade func (p *PriceOracle) calculateV2PriceImpact(amountIn, reserveIn, reserveOut *big.Int) float64 { if reserveIn.Sign() == 0 || reserveOut.Sign() == 0 { return 0 } // Price before = reserveOut / reserveIn priceBefore := new(big.Float).Quo(new(big.Float).SetInt(reserveOut), new(big.Float).SetInt(reserveIn)) // Calculate new reserves after trade amountInWithFee := new(big.Int).Mul(amountIn, big.NewInt(997)) newReserveIn := new(big.Int).Add(reserveIn, new(big.Int).Div(amountInWithFee, big.NewInt(1000))) numerator := new(big.Int).Mul(amountInWithFee, reserveOut) denominator := new(big.Int).Add(new(big.Int).Mul(reserveIn, big.NewInt(1000)), amountInWithFee) amountOut := new(big.Int).Div(numerator, denominator) newReserveOut := new(big.Int).Sub(reserveOut, amountOut) // Price after = newReserveOut / newReserveIn priceAfter := new(big.Float).Quo(new(big.Float).SetInt(newReserveOut), new(big.Float).SetInt(newReserveIn)) // Price impact = |priceAfter - priceBefore| / priceBefore priceDiff := new(big.Float).Sub(priceAfter, priceBefore) priceDiff.Abs(priceDiff) impact := new(big.Float).Quo(priceDiff, priceBefore) impactFloat, _ := impact.Float64() return impactFloat } // PoolState represents the current state of a Uniswap V3 pool type PoolState struct { SqrtPriceX96 *big.Int Tick int32 Liquidity *big.Int Token0 common.Address Token1 common.Address } // getPoolState retrieves the current state of a Uniswap V3 pool func (p *PriceOracle) getPoolState(ctx context.Context, poolAddr common.Address) (*PoolState, error) { // Uniswap V3 Pool ABI (slot0 function) poolABI := `[{"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"}]` contractABI, err := uniswap.ParseABI(poolABI) if err != nil { return nil, fmt.Errorf("failed to parse pool ABI: %w", err) } // Get slot0 data slot0Data, err := contractABI.Pack("slot0") if err != nil { return nil, fmt.Errorf("failed to pack slot0 call: %w", err) } slot0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: slot0Data, }, nil) if err != nil { return nil, fmt.Errorf("slot0 call failed: %w", err) } slot0Unpacked, err := contractABI.Unpack("slot0", slot0Result) if err != nil { return nil, fmt.Errorf("failed to unpack slot0: %w", err) } // Get liquidity liquidityData, err := contractABI.Pack("liquidity") if err != nil { return nil, fmt.Errorf("failed to pack liquidity call: %w", err) } liquidityResult, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: liquidityData, }, nil) if err != nil { return nil, fmt.Errorf("liquidity call failed: %w", err) } liquidityUnpacked, err := contractABI.Unpack("liquidity", liquidityResult) if err != nil { return nil, fmt.Errorf("failed to unpack liquidity: %w", err) } // Get token addresses token0Data, err := contractABI.Pack("token0") if err != nil { return nil, fmt.Errorf("failed to pack token0 call: %w", err) } token0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: token0Data, }, nil) if err != nil { return nil, fmt.Errorf("token0 call failed: %w", err) } token0Unpacked, err := contractABI.Unpack("token0", token0Result) if err != nil { return nil, fmt.Errorf("failed to unpack token0: %w", err) } token1Data, err := contractABI.Pack("token1") if err != nil { return nil, fmt.Errorf("failed to pack token1 call: %w", err) } token1Result, err := p.client.CallContract(ctx, ethereum.CallMsg{ To: &poolAddr, Data: token1Data, }, nil) if err != nil { return nil, fmt.Errorf("token1 call failed: %w", err) } token1Unpacked, err := contractABI.Unpack("token1", token1Result) if err != nil { return nil, fmt.Errorf("failed to unpack token1: %w", err) } // Extract values with proper type conversion sqrtPriceX96, ok := slot0Unpacked[0].(*big.Int) if !ok { return nil, fmt.Errorf("invalid sqrtPriceX96 type: %T", slot0Unpacked[0]) } // Convert tick from interface to int32 var tick int32 switch v := slot0Unpacked[1].(type) { case *big.Int: tick = int32(v.Int64()) case int32: tick = v default: return nil, fmt.Errorf("invalid tick type: %T", slot0Unpacked[1]) } liquidity, ok := liquidityUnpacked[0].(*big.Int) if !ok { return nil, fmt.Errorf("invalid liquidity type: %T", liquidityUnpacked[0]) } token0, ok := token0Unpacked[0].(common.Address) if !ok { return nil, fmt.Errorf("invalid token0 type: %T", token0Unpacked[0]) } token1, ok := token1Unpacked[0].(common.Address) if !ok { return nil, fmt.Errorf("invalid token1 type: %T", token1Unpacked[0]) } return &PoolState{ SqrtPriceX96: sqrtPriceX96, Tick: tick, Liquidity: liquidity, Token0: token0, Token1: token1, }, nil } // calculateSlippage calculates slippage in basis points func (p *PriceOracle) calculateSlippage(amountIn, amountOut, currentPrice *big.Int) (*big.Int, error) { if amountIn.Sign() == 0 || currentPrice.Sign() == 0 { return big.NewInt(0), nil } // Expected amount out at current price expectedOut := new(big.Int).Mul(amountIn, currentPrice) expectedOut.Div(expectedOut, big.NewInt(1e18)) // Calculate slippage percentage if expectedOut.Sign() == 0 { return big.NewInt(0), nil } // Slippage = (expectedOut - actualOut) / expectedOut * 10000 (basis points) diff := new(big.Int).Sub(expectedOut, amountOut) slippage := new(big.Int).Mul(diff, big.NewInt(10000)) slippage.Div(slippage, expectedOut) // Ensure non-negative slippage if slippage.Sign() < 0 { slippage.Neg(slippage) } return slippage, nil } // cachePrice stores a price in the cache func (p *PriceOracle) cachePrice(req *PriceRequest, response *PriceResponse) { key := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex()) p.cacheMutex.Lock() defer p.cacheMutex.Unlock() p.priceCache[key] = &PriceData{ Price: response.Price, Timestamp: response.Timestamp, Source: response.Source, Confidence: 1.0, // Full confidence for fresh data } } // getCachedPrice retrieves a cached price if available and not expired func (p *PriceOracle) getCachedPrice(req *PriceRequest) *PriceResponse { key := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex()) p.cacheMutex.RLock() defer p.cacheMutex.RUnlock() cached, exists := p.priceCache[key] if !exists { return nil } // Check if cache is expired if time.Since(cached.Timestamp) > p.cacheExpiry { return nil } // Calculate amount out using cached price amountOut := new(big.Int).Mul(req.AmountIn, cached.Price) amountOut.Div(amountOut, big.NewInt(1e18)) return &PriceResponse{ Price: cached.Price, AmountOut: amountOut, Source: cached.Source + "_cached", Timestamp: cached.Timestamp, Valid: true, } } // backgroundUpdater runs periodic price updates func (p *PriceOracle) backgroundUpdater() { for { select { case <-p.updateTicker.C: p.updatePriceCache() case <-p.stopChan: return } } } // updatePriceCache updates cached prices for major pairs func (p *PriceOracle) updatePriceCache() { // Update prices for major trading pairs majorPairs := []struct { tokenIn common.Address tokenOut common.Address }{ // Add major pairs here based on your configuration } for _, pair := range majorPairs { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) req := &PriceRequest{ TokenIn: pair.tokenIn, TokenOut: pair.tokenOut, AmountIn: big.NewInt(1e18), // 1 token Timestamp: time.Now(), } if response, err := p.GetPrice(ctx, req); err == nil { p.cachePrice(req, response) } cancel() } } // Stop stops the price oracle and cleanup resources func (p *PriceOracle) Stop() { if p.updateTicker != nil { p.updateTicker.Stop() } close(p.stopChan) } // getChainlinkFeeds returns the mapping of tokens to Chainlink price feeds func getChainlinkFeeds() map[common.Address]common.Address { return map[common.Address]common.Address{ // Add Chainlink feed addresses for major tokens // Example: WETH -> ETH/USD feed common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): common.HexToAddress("0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612"), // WETH/USD common.HexToAddress("0xA0b862F60edEf4452F25B4160F177db44DeB6Cf1"): common.HexToAddress("0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3"), // GNO/USD // Add more feeds as needed } } // getUniswapPools returns the mapping of token pairs to Uniswap pool addresses func getUniswapPools() map[string]common.Address { return map[string]common.Address{ // Add Uniswap V3 pool addresses for major pairs // Format: "token0-token1" -> pool address // Use lowercase hex addresses for consistency // Example: WETH-USDC pool "0x82af49447d8a07e3bd95bd0d56f35241523fbab1-0xa0b862f60edef4452f25b4160f177db44deb6cf1": common.HexToAddress("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"), // WETH-GNO // Add more pools as needed } }