From 93f98863546d60486a6791930716994d3eba9c54 Mon Sep 17 00:00:00 2001 From: dwasse Date: Mon, 14 Oct 2024 10:18:13 -0500 Subject: [PATCH] feat(rfq-relayer): add MaxRelayAmount (#3259) * Feat: add quoteParams helper for test * Feat: add MaxQuoteAmount to relconfig * Feat: use MaxQuoteAmount * Feat: handle MaxQuoteAmount in quoter test * Replace: MaxQuoteAmount -> MaxRelayAmount * Feat: shouldProcess() returns false if max relay amount exceeded * Feat: add test for MaxRelayAmount --- services/rfq/relayer/quoter/quoter.go | 21 +++++- services/rfq/relayer/quoter/quoter_test.go | 82 +++++++++++++++++++--- services/rfq/relayer/relconfig/config.go | 2 + services/rfq/relayer/relconfig/getters.go | 35 +++++++++ 4 files changed, 128 insertions(+), 12 deletions(-) diff --git a/services/rfq/relayer/quoter/quoter.go b/services/rfq/relayer/quoter/quoter.go index 3b5ecc8352..2cd3dfcf94 100644 --- a/services/rfq/relayer/quoter/quoter.go +++ b/services/rfq/relayer/quoter/quoter.go @@ -207,6 +207,15 @@ func (m *Manager) ShouldProcess(parentCtx context.Context, quote reldb.QuoteRequ return false, nil } + // check relay amount + maxRelayAmount := m.config.GetMaxRelayAmount(int(quote.Transaction.OriginChainId), quote.Transaction.OriginToken) + if maxRelayAmount != nil { + if quote.Transaction.OriginAmount.Cmp(maxRelayAmount) > 0 { + span.AddEvent("origin amount is greater than max relay amount") + return false, nil + } + } + // all checks have passed return true, nil } @@ -713,7 +722,7 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, input QuoteInput) ( } } - // Finally, clip the quoteAmount by the dest balance + // Clip the quoteAmount by the dest balance if quoteAmount.Cmp(input.DestBalance) > 0 { span.AddEvent("quote amount greater than destination balance", trace.WithAttributes( attribute.String("quote_amount", quoteAmount.String()), @@ -722,6 +731,16 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, input QuoteInput) ( quoteAmount = input.DestBalance } + // Clip the quoteAmount by the maxQuoteAmount + maxQuoteAmount := m.config.GetMaxRelayAmount(input.DestChainID, input.DestTokenAddr) + if maxQuoteAmount != nil && quoteAmount.Cmp(maxQuoteAmount) > 0 { + span.AddEvent("quote amount greater than max quote amount", trace.WithAttributes( + attribute.String("quote_amount", quoteAmount.String()), + attribute.String("max_quote_amount", maxQuoteAmount.String()), + )) + quoteAmount = maxQuoteAmount + } + // Deduct gas cost from the quote amount, if necessary quoteAmount, err = m.deductGasCost(ctx, quoteAmount, input.DestTokenAddr, input.DestChainID) if err != nil { diff --git a/services/rfq/relayer/quoter/quoter_test.go b/services/rfq/relayer/quoter/quoter_test.go index 1d6a52c7de..321d55b189 100644 --- a/services/rfq/relayer/quoter/quoter_test.go +++ b/services/rfq/relayer/quoter/quoter_test.go @@ -136,6 +136,13 @@ func (s *QuoterSuite) TestShouldProcess() { s.False(s.manager.ShouldProcess(s.GetTestContext(), quote)) s.manager.SetRelayPaused(false) s.True(s.manager.ShouldProcess(s.GetTestContext(), quote)) + + // Set max relay amount + originTokenCfg := s.config.Chains[int(s.origin)].Tokens["USDC"] + originTokenCfg.MaxRelayAmount = "900" // less than balance + s.config.Chains[int(s.origin)].Tokens["USDC"] = originTokenCfg + s.manager.SetConfig(s.config) + s.False(s.manager.ShouldProcess(s.GetTestContext(), quote)) } func (s *QuoterSuite) TestIsProfitable() { @@ -173,13 +180,23 @@ func (s *QuoterSuite) TestGetOriginAmount() { originAddr := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") balance := big.NewInt(1000_000_000) // 1000 USDC - setQuoteParams := func(quotePct, quoteOffset float64, minQuoteAmount, maxBalance string) { - s.config.BaseChainConfig.QuotePct = "ePct + type quoteParams struct { + quotePct float64 + quoteOffset float64 + minQuoteAmount string + maxBalance string + maxQuoteAmount string + } + + setQuoteParams := func(params quoteParams) { + s.config.BaseChainConfig.QuotePct = ¶ms.quotePct destTokenCfg := s.config.Chains[dest].Tokens["USDC"] - destTokenCfg.MinQuoteAmount = minQuoteAmount + destTokenCfg.MinQuoteAmount = params.minQuoteAmount + destTokenCfg.MaxRelayAmount = params.maxQuoteAmount originTokenCfg := s.config.Chains[origin].Tokens["USDC"] - originTokenCfg.QuoteOffsetBps = quoteOffset - originTokenCfg.MaxBalance = &maxBalance + originTokenCfg.QuoteOffsetBps = params.quoteOffset + originTokenCfg.MaxBalance = ¶ms.maxBalance + originTokenCfg.MaxRelayAmount = params.maxQuoteAmount s.config.Chains[dest].Tokens["USDC"] = destTokenCfg s.config.Chains[origin].Tokens["USDC"] = originTokenCfg s.manager.SetConfig(s.config) @@ -201,42 +218,85 @@ func (s *QuoterSuite) TestGetOriginAmount() { s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 50 with MinQuoteAmount of 0; should be 50% of balance. - setQuoteParams(50, 0, "0", "0") + setQuoteParams(quoteParams{ + quotePct: 50, + quoteOffset: 0, + minQuoteAmount: "0", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) 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(50, -100, "0", "0") + 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(25, 0, "500", "0") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "500", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(500_000_000) s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 25 with MinQuoteAmount of 500; should be 50% of balance. - setQuoteParams(25, 0, "500", "0") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "500", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(500_000_000) s.Equal(expectedAmount, quoteAmount) // Set QuotePct to 25 with MinQuoteAmount of 1500; should be total balance. - setQuoteParams(25, 0, "1500", "0") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "1500", + maxBalance: "0", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(1000_000_000) s.Equal(expectedAmount, quoteAmount) + // Set QuotePct to 100 with MinQuoteAmount of 0 and MaxRelayAmount of 500; should be 500. + setQuoteParams(quoteParams{ + quotePct: 100, + quoteOffset: 0, + minQuoteAmount: "0", + maxBalance: "0", + maxQuoteAmount: "500", + }) + quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) + s.NoError(err) + expectedAmount = big.NewInt(500_000_000) + s.Equal(expectedAmount, quoteAmount) + // Set QuotePct to 25 with MinQuoteAmount of 1500 and MaxBalance of 1200; should be 200. - setQuoteParams(25, 0, "1500", "1200") + setQuoteParams(quoteParams{ + quotePct: 25, + quoteOffset: 0, + minQuoteAmount: "1500", + maxBalance: "1200", + }) quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input) s.NoError(err) expectedAmount = big.NewInt(200_000_000) diff --git a/services/rfq/relayer/relconfig/config.go b/services/rfq/relayer/relconfig/config.go index a4449bf8db..220627e13a 100644 --- a/services/rfq/relayer/relconfig/config.go +++ b/services/rfq/relayer/relconfig/config.go @@ -124,6 +124,8 @@ type TokenConfig struct { PriceUSD float64 `yaml:"price_usd"` // MinQuoteAmount is the minimum amount to quote for this token in human-readable units. MinQuoteAmount string `yaml:"min_quote_amount"` + // MaxRelayAmount is the maximum amount to quote and relay for this token in human-readable units. + MaxRelayAmount string `yaml:"max_relay_amount"` // RebalanceMethods are the supported methods for rebalancing. RebalanceMethods []string `yaml:"rebalance_methods"` // MaintenanceBalancePct is the percentage of the total balance under which a rebalance will be triggered. diff --git a/services/rfq/relayer/relconfig/getters.go b/services/rfq/relayer/relconfig/getters.go index 2cb4880712..a3fbb25cc7 100644 --- a/services/rfq/relayer/relconfig/getters.go +++ b/services/rfq/relayer/relconfig/getters.go @@ -746,6 +746,41 @@ func (c Config) GetMinQuoteAmount(chainID int, addr common.Address) *big.Int { return quoteAmountScaled } +var defaultMaxRelayAmount *big.Int // nil + +// GetMaxRelayAmount returns the quote amount for the given chain and address. +// Note that this getter returns the value in native token decimals. +func (c Config) GetMaxRelayAmount(chainID int, addr common.Address) *big.Int { + chainCfg, ok := c.Chains[chainID] + if !ok { + return defaultMaxRelayAmount + } + + var tokenCfg *TokenConfig + for _, cfg := range chainCfg.Tokens { + if common.HexToAddress(cfg.Address).Hex() == addr.Hex() { + cfgCopy := cfg + tokenCfg = &cfgCopy + break + } + } + if tokenCfg == nil { + return defaultMaxRelayAmount + } + quoteAmountFlt, ok := new(big.Float).SetString(tokenCfg.MaxRelayAmount) + if !ok { + return defaultMaxRelayAmount + } + if quoteAmountFlt.Cmp(big.NewFloat(0)) <= 0 { + return defaultMaxRelayAmount + } + + // Scale the minQuoteAmount by the token decimals. + denomDecimalsFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(tokenCfg.Decimals)), nil) + quoteAmountScaled, _ := new(big.Float).Mul(quoteAmountFlt, new(big.Float).SetInt(denomDecimalsFactor)).Int(nil) + return quoteAmountScaled +} + var defaultMinRebalanceAmount = big.NewInt(1000) // GetMinRebalanceAmount returns the min rebalance amount for the given chain and address.