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" ) // Curve StableSwap TokenExchange event signature: // event TokenExchange(address indexed buyer, int128 sold_id, uint256 tokens_sold, int128 bought_id, uint256 tokens_bought) var ( // CurveTokenExchangeSignature is the event signature for Curve TokenExchange events CurveTokenExchangeSignature = crypto.Keccak256Hash([]byte("TokenExchange(address,int128,uint256,int128,uint256)")) // CurveTokenExchangeUnderlyingSignature is for pools with underlying tokens CurveTokenExchangeUnderlyingSignature = crypto.Keccak256Hash([]byte("TokenExchangeUnderlying(address,int128,uint256,int128,uint256)")) ) // CurveParser implements the Parser interface for Curve StableSwap pools type CurveParser struct { cache cache.PoolCache logger mevtypes.Logger } // NewCurveParser creates a new Curve parser func NewCurveParser(cache cache.PoolCache, logger mevtypes.Logger) *CurveParser { return &CurveParser{ cache: cache, logger: logger, } } // Protocol returns the protocol type this parser handles func (p *CurveParser) Protocol() mevtypes.ProtocolType { return mevtypes.ProtocolCurve } // SupportsLog checks if this parser can handle the given log func (p *CurveParser) SupportsLog(log types.Log) bool { // Check if log has the TokenExchange or TokenExchangeUnderlying event signature if len(log.Topics) == 0 { return false } return log.Topics[0] == CurveTokenExchangeSignature || log.Topics[0] == CurveTokenExchangeUnderlyingSignature } // ParseLog parses a Curve TokenExchange event from a log func (p *CurveParser) ParseLog(ctx context.Context, log types.Log, tx *types.Transaction) (*mevtypes.SwapEvent, error) { // Verify this is a TokenExchange 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: sold_id, tokens_sold, bought_id, tokens_bought (non-indexed) // Topics contain: [signature, buyer] (indexed) if len(log.Topics) != 2 { return nil, fmt.Errorf("invalid number of topics: expected 2, got %d", len(log.Topics)) } // Define ABI for data decoding int128Type, err := abi.NewType("int128", "", nil) if err != nil { return nil, fmt.Errorf("failed to create int128 type: %w", err) } uint256Type, err := abi.NewType("uint256", "", nil) if err != nil { return nil, fmt.Errorf("failed to create uint256 type: %w", err) } arguments := abi.Arguments{ {Type: int128Type, Name: "sold_id"}, {Type: uint256Type, Name: "tokens_sold"}, {Type: int128Type, Name: "bought_id"}, {Type: uint256Type, Name: "tokens_bought"}, } // 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) != 4 { return nil, fmt.Errorf("invalid number of values: expected 4, got %d", len(values)) } // Extract buyer from topics buyer := common.BytesToAddress(log.Topics[1].Bytes()) // Extract coin indices and amounts soldID := values[0].(*big.Int) tokensSold := values[1].(*big.Int) boughtID := values[2].(*big.Int) tokensBought := values[3].(*big.Int) // Convert coin indices to uint soldIndex := int(soldID.Int64()) boughtIndex := int(boughtID.Int64()) // Determine which token is token0 and token1 // Curve pools typically have 2-4 coins, we'll handle the common case of 2 coins var token0, token1 common.Address var token0Decimals, token1Decimals uint8 var amount0In, amount1In, amount0Out, amount1Out *big.Int // Map coin indices to tokens // For simplicity, we assume sold_id < bought_id means token0 → token1 if soldIndex == 0 && boughtIndex == 1 { // Selling token0 for token1 token0 = poolInfo.Token0 token1 = poolInfo.Token1 token0Decimals = poolInfo.Token0Decimals token1Decimals = poolInfo.Token1Decimals amount0In = tokensSold amount1In = big.NewInt(0) amount0Out = big.NewInt(0) amount1Out = tokensBought } else if soldIndex == 1 && boughtIndex == 0 { // Selling token1 for token0 token0 = poolInfo.Token0 token1 = poolInfo.Token1 token0Decimals = poolInfo.Token0Decimals token1Decimals = poolInfo.Token1Decimals amount0In = big.NewInt(0) amount1In = tokensSold amount0Out = tokensBought amount1Out = big.NewInt(0) } else { // For multi-coin pools (3+ coins), we need more complex logic // For now, we'll use the pool's token0 and token1 as defaults token0 = poolInfo.Token0 token1 = poolInfo.Token1 token0Decimals = poolInfo.Token0Decimals token1Decimals = poolInfo.Token1Decimals // Assume if sold_id is 0, we're selling token0 if soldIndex == 0 { amount0In = tokensSold amount1In = big.NewInt(0) amount0Out = big.NewInt(0) amount1Out = tokensBought } else { amount0In = big.NewInt(0) amount1In = tokensSold amount0Out = tokensBought amount1Out = big.NewInt(0) } } // Scale amounts to 18 decimals for internal representation amount0InScaled := mevtypes.ScaleToDecimals(amount0In, token0Decimals, 18) amount1InScaled := mevtypes.ScaleToDecimals(amount1In, token1Decimals, 18) amount0OutScaled := mevtypes.ScaleToDecimals(amount0Out, token0Decimals, 18) amount1OutScaled := mevtypes.ScaleToDecimals(amount1Out, token1Decimals, 18) // Create swap event event := &mevtypes.SwapEvent{ TxHash: tx.Hash(), BlockNumber: log.BlockNumber, LogIndex: uint(log.Index), PoolAddress: log.Address, Protocol: mevtypes.ProtocolCurve, Token0: token0, Token1: token1, Token0Decimals: token0Decimals, Token1Decimals: token1Decimals, Amount0In: amount0InScaled, Amount1In: amount1InScaled, Amount0Out: amount0OutScaled, Amount1Out: amount1OutScaled, Sender: buyer, Recipient: buyer, // In Curve, buyer is both sender and recipient Fee: big.NewInt(int64(poolInfo.Fee)), // Curve pools have variable fees } // Validate the parsed event if err := event.Validate(); err != nil { return nil, fmt.Errorf("validation failed: %w", err) } p.logger.Debug("parsed Curve swap event", "txHash", event.TxHash.Hex(), "pool", event.PoolAddress.Hex(), "soldID", soldIndex, "boughtID", boughtIndex, "tokensSold", tokensSold.String(), "tokensBought", tokensBought.String(), ) return event, nil } // ParseReceipt parses all Curve TokenExchange events from a transaction receipt func (p *CurveParser) 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 }