diff --git a/services/rfq/relayer/quoter/export_test.go b/services/rfq/relayer/quoter/export_test.go index 66817a0ce5..9f12c9477d 100644 --- a/services/rfq/relayer/quoter/export_test.go +++ b/services/rfq/relayer/quoter/export_test.go @@ -18,8 +18,8 @@ func (m *Manager) GetOriginAmount(ctx context.Context, input QuoteInput) (*big.I return m.getOriginAmount(ctx, input) } -func (m *Manager) GetDestAmount(ctx context.Context, quoteAmount *big.Int, chainID int, tokenName string) (*big.Int, error) { - return m.getDestAmount(ctx, quoteAmount, chainID, tokenName) +func (m *Manager) GetDestAmount(ctx context.Context, quoteAmount *big.Int, tokenName string, input QuoteInput) (*big.Int, error) { + return m.getDestAmount(ctx, quoteAmount, tokenName, input) } func (m *Manager) SetConfig(cfg relconfig.Config) { diff --git a/services/rfq/relayer/quoter/quoter.go b/services/rfq/relayer/quoter/quoter.go index 2cd3dfcf94..fb5ef025e2 100644 --- a/services/rfq/relayer/quoter/quoter.go +++ b/services/rfq/relayer/quoter/quoter.go @@ -237,16 +237,44 @@ func (m *Manager) IsProfitable(parentCtx context.Context, quote reldb.QuoteReque if err != nil { return false, fmt.Errorf("error getting total fee: %w", err) } - cost := new(big.Int).Add(quote.Transaction.DestAmount, fee) - span.AddEvent("fee", trace.WithAttributes(attribute.String("fee", fee.String()))) - span.AddEvent("cost", trace.WithAttributes(attribute.String("cost", cost.String()))) - span.AddEvent("dest_amount", trace.WithAttributes(attribute.String("dest_amount", quote.Transaction.DestAmount.String()))) - span.AddEvent("origin_amount", trace.WithAttributes(attribute.String("origin_amount", quote.Transaction.OriginAmount.String()))) + // adjust amounts for our internal offsets on origin / dest token values + originAmountAdj, err := m.getAmountWithOffset(ctx, quote.Transaction.OriginChainId, quote.Transaction.OriginToken, quote.Transaction.OriginAmount) + if err != nil { + return false, fmt.Errorf("error getting origin amount with offset: %w", err) + } + // assume that fee is denominated in dest token terms + costAdj, err := m.getAmountWithOffset(ctx, quote.Transaction.DestChainId, quote.Transaction.DestToken, cost) + if err != nil { + return false, fmt.Errorf("error getting cost with offset: %w", err) + } + + span.SetAttributes( + attribute.String("origin_amount_adj", originAmountAdj.String()), + attribute.String("cost_adj", costAdj.String()), + attribute.String("origin_amount", quote.Transaction.OriginAmount.String()), + attribute.String("dest_amount", quote.Transaction.DestAmount.String()), + attribute.String("fee", fee.String()), + attribute.String("cost", cost.String()), + ) - // NOTE: this logic assumes that the origin and destination tokens have the same price. - return quote.Transaction.OriginAmount.Cmp(cost) >= 0, nil + return originAmountAdj.Cmp(costAdj) >= 0, nil +} + +func (m *Manager) getAmountWithOffset(ctx context.Context, chainID uint32, tokenAddr common.Address, amount *big.Int) (*big.Int, error) { + tokenName, err := m.config.GetTokenName(chainID, tokenAddr.Hex()) + if err != nil { + return nil, fmt.Errorf("error getting token name: %w", err) + } + // apply offset directly to amount without considering origin/dest + quoteOffsetBps, err := m.config.GetQuoteOffsetBps(int(chainID), tokenName, true) + if err != nil { + return nil, fmt.Errorf("error getting quote offset bps: %w", err) + } + amountAdj := m.applyOffset(ctx, quoteOffsetBps, amount) + + return amountAdj, nil } // SubmitAllQuotes submits all quotes to the RFQ API. @@ -580,7 +608,7 @@ func (m *Manager) generateQuote(ctx context.Context, input QuoteInput) (quote *m } // Build the quote - destAmount, err := m.getDestAmount(ctx, originAmount, input.DestChainID, destToken) + destAmount, err := m.getDestAmount(ctx, originAmount, destToken, input) if err != nil { logger.Error("Error getting dest amount", "error", err) return nil, fmt.Errorf("error getting dest amount: %w", err) @@ -679,17 +707,6 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, input QuoteInput) ( balanceFlt := new(big.Float).SetInt(input.DestBalance) quoteAmount, _ = new(big.Float).Mul(balanceFlt, new(big.Float).SetFloat64(quotePct/100)).Int(nil) - // Apply the quoteOffset to origin token. - tokenName, err := m.config.GetTokenName(uint32(input.DestChainID), input.DestTokenAddr.Hex()) - if err != nil { - return nil, fmt.Errorf("error getting token name: %w", err) - } - quoteOffsetBps, err := m.config.GetQuoteOffsetBps(input.OriginChainID, tokenName, true) - if err != nil { - return nil, fmt.Errorf("error getting quote offset bps: %w", err) - } - quoteAmount = m.applyOffset(ctx, quoteOffsetBps, quoteAmount) - // Clip the quoteAmount by the minQuoteAmount minQuoteAmount := m.config.GetMinQuoteAmount(input.DestChainID, input.DestTokenAddr) if quoteAmount.Cmp(minQuoteAmount) < 0 { @@ -784,7 +801,7 @@ func (m *Manager) deductGasCost(parentCtx context.Context, quoteAmount *big.Int, var errMinGasExceedsQuoteAmount = errors.New("min gas token exceeds quote amount") -func (m *Manager) getDestAmount(parentCtx context.Context, originAmount *big.Int, chainID int, tokenName string) (*big.Int, error) { +func (m *Manager) getDestAmount(parentCtx context.Context, originAmount *big.Int, tokenName string, input QuoteInput) (*big.Int, error) { ctx, span := m.metricsHandler.Tracer().Start(parentCtx, "getDestAmount", trace.WithAttributes( attribute.String("quote_amount", originAmount.String()), )) @@ -792,20 +809,27 @@ func (m *Manager) getDestAmount(parentCtx context.Context, originAmount *big.Int metrics.EndSpan(span) }() - quoteOffsetBps, err := m.config.GetQuoteOffsetBps(chainID, tokenName, false) + // Apply origin, destination, and quote width offsets + originOffsetBps, err := m.config.GetQuoteOffsetBps(input.OriginChainID, tokenName, true) + if err != nil { + return nil, fmt.Errorf("error getting quote offset bps: %w", err) + } + destOffsetBps, err := m.config.GetQuoteOffsetBps(input.DestChainID, tokenName, false) if err != nil { return nil, fmt.Errorf("error getting quote offset bps: %w", err) } - quoteWidthBps, err := m.config.GetQuoteWidthBps(chainID) + quoteWidthBps, err := m.config.GetQuoteWidthBps(input.DestChainID) if err != nil { return nil, fmt.Errorf("error getting quote width bps: %w", err) } - totalOffsetBps := quoteOffsetBps + quoteWidthBps + totalOffsetBps := originOffsetBps + destOffsetBps + quoteWidthBps destAmount := m.applyOffset(ctx, totalOffsetBps, originAmount) span.SetAttributes( - attribute.Float64("quote_offset_bps", quoteOffsetBps), + attribute.Float64("origin_offset_bps", originOffsetBps), + attribute.Float64("dest_offset_bps", destOffsetBps), attribute.Float64("quote_width_bps", quoteWidthBps), + attribute.Float64("total_offset_bps", totalOffsetBps), attribute.String("dest_amount", destAmount.String()), ) return destAmount, nil diff --git a/services/rfq/relayer/quoter/quoter_test.go b/services/rfq/relayer/quoter/quoter_test.go index 321d55b189..48a5b1f19b 100644 --- a/services/rfq/relayer/quoter/quoter_test.go +++ b/services/rfq/relayer/quoter/quoter_test.go @@ -171,6 +171,35 @@ func (s *QuoterSuite) TestIsProfitable() { // Set fee to less than breakeven; i.e. destAmount < originAmount - fee. quote.Transaction.DestAmount = balance s.False(s.manager.IsProfitable(s.GetTestContext(), quote)) + + origin := int(s.origin) + dest := int(s.destination) + setQuoteOffsets := func(originOffset, destOffset float64) { + originTokenCfg := s.config.Chains[origin].Tokens["USDC"] + originTokenCfg.QuoteOffsetBps = originOffset + s.config.Chains[origin].Tokens["USDC"] = originTokenCfg + destTokenCfg := s.config.Chains[dest].Tokens["USDC"] + destTokenCfg.QuoteOffsetBps = destOffset + s.config.Chains[dest].Tokens["USDC"] = destTokenCfg + s.manager.SetConfig(s.config) + } + quote.Transaction.DestAmount = new(big.Int).Sub(balance, fee) + + // Set dest offset to 20%; we send a token that is more valuable -> not profitable + setQuoteOffsets(0, 2000) + s.False(s.manager.IsProfitable(s.GetTestContext(), quote)) + + // Set dest offset to -20%; we send a token that is less valuable -> profitable + setQuoteOffsets(0, -2000) + s.True(s.manager.IsProfitable(s.GetTestContext(), quote)) + + // Set origin offset to 20%; we get a token that is more valuable -> not profitable + setQuoteOffsets(2000, 0) + s.True(s.manager.IsProfitable(s.GetTestContext(), quote)) + + // Set origin offset to -20%; we send a token that is less valuable -> not profitable + setQuoteOffsets(-2000, 0) + s.False(s.manager.IsProfitable(s.GetTestContext(), quote)) } func (s *QuoterSuite) TestGetOriginAmount() { @@ -229,18 +258,6 @@ func (s *QuoterSuite) TestGetOriginAmount() { expectedAmount = big.NewInt(500_000_000) s.Equal(expectedAmount, quoteAmount) - // Set QuotePct to 50 with QuoteOffset of -1%. Should be 1% less than 50% of balance. - setQuoteParams(quoteParams{ - quotePct: 50, - quoteOffset: -100, - minQuoteAmount: "0", - maxBalance: "0", - }) - quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) - s.NoError(err) - expectedAmount = big.NewInt(495_000_000) - s.Equal(expectedAmount, quoteAmount) - // Set QuotePct to 25 with MinQuoteAmount of 500; should be 50% of balance. setQuoteParams(quoteParams{ quotePct: 25, @@ -328,53 +345,84 @@ func (s *QuoterSuite) setGasSufficiency(sufficient bool) { func (s *QuoterSuite) TestGetDestAmount() { balance := big.NewInt(1000_000_000) // 1000 USDC - chainID := int(s.destination) - setQuoteParams := func(quoteOffsetBps, quoteWidthBps float64) { + origin := int(s.origin) + dest := int(s.destination) + input := quoter.QuoteInput{ + OriginChainID: int(s.origin), + DestChainID: int(s.destination), + OriginBalance: balance, + DestBalance: balance, + } + setQuoteParams := func(originQuoteOffsetBps, destQuoteOffsetBps, quoteWidthBps float64) { s.config.BaseChainConfig.QuoteWidthBps = quoteWidthBps - tokenCfg := s.config.Chains[chainID].Tokens["USDC"] - tokenCfg.QuoteOffsetBps = quoteOffsetBps - s.config.Chains[chainID].Tokens["USDC"] = tokenCfg + tokenCfg := s.config.Chains[origin].Tokens["USDC"] + tokenCfg.QuoteOffsetBps = originQuoteOffsetBps + s.config.Chains[origin].Tokens["USDC"] = tokenCfg + tokenCfg = s.config.Chains[dest].Tokens["USDC"] + tokenCfg.QuoteOffsetBps = destQuoteOffsetBps + s.config.Chains[dest].Tokens["USDC"] = tokenCfg s.manager.SetConfig(s.config) } // Set default quote params; should return the balance. - destAmount, err := s.manager.GetDestAmount(s.GetTestContext(), balance, chainID, "USDC") + destAmount, err := s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) s.NoError(err) expectedAmount := balance s.Equal(expectedAmount, destAmount) // Set QuoteWidthBps to 100, should return 99% of balance. - setQuoteParams(0, 100) - destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, chainID, "USDC") + setQuoteParams(0, 0, 100) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) s.NoError(err) expectedAmount = big.NewInt(990_000_000) s.Equal(expectedAmount, destAmount) // Set QuoteWidthBps to 500, should return 95% of balance. - setQuoteParams(0, 500) - destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, chainID, "USDC") + setQuoteParams(0, 0, 500) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) s.NoError(err) expectedAmount = big.NewInt(950_000_000) s.Equal(expectedAmount, destAmount) // Set QuoteWidthBps to 500 and QuoteOffsetBps to 100, should return 94% of balance. - setQuoteParams(100, 500) - destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, chainID, "USDC") + setQuoteParams(0, 100, 500) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) s.NoError(err) expectedAmount = big.NewInt(940_000_000) s.Equal(expectedAmount, destAmount) // Set QuoteWidthBps to 500 and QuoteOffsetBps to -100, should return 96% of balance. - setQuoteParams(-100, 500) - destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, chainID, "USDC") + setQuoteParams(0, -100, 500) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) s.NoError(err) expectedAmount = big.NewInt(960_000_000) s.Equal(expectedAmount, destAmount) // Set QuoteWidthBps to -100, should default to balance. - setQuoteParams(0, -100) - destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, chainID, "USDC") + setQuoteParams(0, 0, -100) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) s.NoError(err) expectedAmount = balance s.Equal(expectedAmount, destAmount) + + // Set origin offset to 100, should return 101% of balance. + setQuoteParams(100, 0, 0) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) + s.NoError(err) + expectedAmount = big.NewInt(1_010_000_000) + s.Equal(expectedAmount, destAmount) + + // Set origin offset to -100, should return 99% of balance. + setQuoteParams(-100, 0, 0) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) + s.NoError(err) + expectedAmount = big.NewInt(990_000_000) + s.Equal(expectedAmount, destAmount) + + // Set origin offset to 100, dest offset to 300, should return 98% of balance. + setQuoteParams(100, 300, 0) + destAmount, err = s.manager.GetDestAmount(s.GetTestContext(), balance, "USDC", input) + s.NoError(err) + expectedAmount = big.NewInt(980_000_000) + s.Equal(expectedAmount, destAmount) }