Skip to content

Commit

Permalink
fix(rfq-relayer): profitability check accounts for offsets (#3288)
Browse files Browse the repository at this point in the history
* Feat: check amounts with offsets in isProfitable()

* WIP: test adj changes

* Fix: quoter tests

* [goreleaser]

* Cleanup: lint

* Feat: move origin offset calc to getDestAmount()

* Feat: add more test cases for new offset logic

* [goreleaser]

* Fix: offset application

* [goreleaser]
  • Loading branch information
dwasse authored Oct 17, 2024
1 parent 791b0ef commit 6880ddd
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 54 deletions.
4 changes: 2 additions & 2 deletions services/rfq/relayer/quoter/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
72 changes: 48 additions & 24 deletions services/rfq/relayer/quoter/quoter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -784,28 +801,35 @@ 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()),
))
defer func() {
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
Expand Down
104 changes: 76 additions & 28 deletions services/rfq/relayer/quoter/quoter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

0 comments on commit 6880ddd

Please sign in to comment.