Skip to content

Commit

Permalink
feat(rfq-relayer): add MaxBalance param (#2917)
Browse files Browse the repository at this point in the history
* Feat: add MinBalance to token config

* Feat: add min balance logic

* Feat: test min balance

* [goreleaser]

* Cleanup: comment

* Feat: MinBalance -> MaxBalance

* Feat: structure quote input

* Feat: move quote outside goroutine for thread safety

* Feat: getOriginAmount() takes origin balance

* Feat: getOriginAmount properly incorporates MaxBalance

* Feat: getOriginQuoteAmount takes QuoteInput struct

* [goreleaser]

* Feat: check for negative quotable balance

* Feat: convert MaxBalance to string ptr

* Feat: handle nil as positive inf

* Cleanup: prints

* Cleanup: tracing msg

* [goreleaser]

* [goreleaser]
  • Loading branch information
dwasse authored Aug 7, 2024
1 parent bf40e0a commit 694626a
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 62 deletions.
8 changes: 4 additions & 4 deletions services/rfq/relayer/quoter/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import (
"github.com/synapsecns/sanguine/services/rfq/relayer/relconfig"
)

func (m *Manager) GenerateQuotes(ctx context.Context, chainID int, address common.Address, balance *big.Int) ([]model.PutQuoteRequest, error) {
func (m *Manager) GenerateQuotes(ctx context.Context, chainID int, address common.Address, balance *big.Int, inv map[int]map[common.Address]*big.Int) ([]model.PutQuoteRequest, error) {
// nolint: errcheck
return m.generateQuotes(ctx, chainID, address, balance)
return m.generateQuotes(ctx, chainID, address, balance, inv)
}

func (m *Manager) GetOriginAmount(ctx context.Context, origin, dest int, address common.Address, balance *big.Int) (*big.Int, error) {
return m.getOriginAmount(ctx, origin, dest, address, balance)
func (m *Manager) GetOriginAmount(ctx context.Context, input QuoteInput) (*big.Int, error) {
return m.getOriginAmount(ctx, input)
}

func (m *Manager) GetDestAmount(ctx context.Context, quoteAmount *big.Int, chainID int, tokenName string) (*big.Int, error) {
Expand Down
135 changes: 94 additions & 41 deletions services/rfq/relayer/quoter/quoter.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ func (m *Manager) prepareAndSubmitQuotes(ctx context.Context, inv map[int]map[co
address := a
balance := b
g.Go(func() error {
quotes, err := m.generateQuotes(gctx, chainID, address, balance)
quotes, err := m.generateQuotes(gctx, chainID, address, balance, inv)
if err != nil {
return fmt.Errorf("error generating quotes: %w", err)
}
Expand Down Expand Up @@ -324,7 +324,7 @@ const meterName = "github.com/synapsecns/sanguine/services/rfq/relayer/quoter"
// Essentially, if we know a destination chain token balance, then we just need to find which tokens are bridgeable to it.
// We can do this by looking at the quotableTokens map, and finding the key that matches the destination chain token.
// Generates quotes for a given chain ID, address, and balance.
func (m *Manager) generateQuotes(parentCtx context.Context, chainID int, address common.Address, balance *big.Int) (quotes []model.PutQuoteRequest, err error) {
func (m *Manager) generateQuotes(parentCtx context.Context, chainID int, address common.Address, balance *big.Int, inv map[int]map[common.Address]*big.Int) (quotes []model.PutQuoteRequest, err error) {
ctx, span := m.metricsHandler.Tracer().Start(parentCtx, "generateQuotes", trace.WithAttributes(
attribute.Int(metrics.Origin, chainID),
attribute.String("address", address.String()),
Expand All @@ -348,9 +348,36 @@ func (m *Manager) generateQuotes(parentCtx context.Context, chainID int, address
for _, tokenID := range itemTokenIDs {
//nolint:nestif
if tokenID == destTokenID {
keyTokenID := k
keyTokenID := k // Parse token info
originStr := strings.Split(keyTokenID, "-")[0]
origin, tokenErr := strconv.Atoi(originStr)
if err != nil {
span.AddEvent("error converting origin chainID", trace.WithAttributes(
attribute.String("key_token_id", keyTokenID),
attribute.String("error", tokenErr.Error()),
))
continue
}
originTokenAddr := common.HexToAddress(strings.Split(keyTokenID, "-")[1])

var originBalance *big.Int
originTokens, ok := inv[origin]
if ok {
originBalance = originTokens[originTokenAddr]
}

g.Go(func() error {
quote, quoteErr := m.generateQuote(gctx, keyTokenID, chainID, address, balance, destRFQAddr.Hex())
input := QuoteInput{
OriginChainID: origin,
DestChainID: chainID,
OriginTokenAddr: originTokenAddr,
DestTokenAddr: address,
OriginBalance: originBalance,
DestBalance: balance,
DestRFQAddr: destRFQAddr.Hex(),
}

quote, quoteErr := m.generateQuote(gctx, input)
if quoteErr != nil {
// continue generating quotes even if one fails
span.AddEvent("error generating quote", trace.WithAttributes(
Expand All @@ -376,18 +403,20 @@ func (m *Manager) generateQuotes(parentCtx context.Context, chainID int, address
return quotes, nil
}

func (m *Manager) generateQuote(ctx context.Context, keyTokenID string, chainID int, address common.Address, balance *big.Int, destRFQAddr string) (quote *model.PutQuoteRequest, err error) {
// Parse token info
originStr := strings.Split(keyTokenID, "-")[0]
origin, err := strconv.Atoi(originStr)
if err != nil {
logger.Error("Error converting origin chainID", "error", err)
return nil, fmt.Errorf("error converting origin chainID: %w", err)
}
originTokenAddr := common.HexToAddress(strings.Split(keyTokenID, "-")[1])
// QuoteInput is a wrapper struct for input arguments to generateQuote.
type QuoteInput struct {
OriginChainID int
DestChainID int
OriginTokenAddr common.Address
DestTokenAddr common.Address
OriginBalance *big.Int
DestBalance *big.Int
DestRFQAddr string
}

func (m *Manager) generateQuote(ctx context.Context, input QuoteInput) (quote *model.PutQuoteRequest, err error) {
// Calculate the quote amount for this route
originAmount, err := m.getOriginAmount(ctx, origin, chainID, address, balance)
originAmount, err := m.getOriginAmount(ctx, input)
// don't quote if gas exceeds quote
if errors.Is(err, errMinGasExceedsQuoteAmount) {
originAmount = big.NewInt(0)
Expand All @@ -397,38 +426,38 @@ func (m *Manager) generateQuote(ctx context.Context, keyTokenID string, chainID
}

// Calculate the fee for this route
destToken, err := m.config.GetTokenName(uint32(chainID), address.Hex())
destToken, err := m.config.GetTokenName(uint32(input.DestChainID), input.DestTokenAddr.Hex())
if err != nil {
logger.Error("Error getting dest token ID", "error", err)
return nil, fmt.Errorf("error getting dest token ID: %w", err)
}
fee, err := m.feePricer.GetTotalFee(ctx, uint32(origin), uint32(chainID), destToken, true)
fee, err := m.feePricer.GetTotalFee(ctx, uint32(input.OriginChainID), uint32(input.DestChainID), destToken, true)
if err != nil {
logger.Error("Error getting total fee", "error", err)
return nil, fmt.Errorf("error getting total fee: %w", err)
}
originRFQAddr, err := m.config.GetRFQAddress(origin)
originRFQAddr, err := m.config.GetRFQAddress(input.OriginChainID)
if err != nil {
logger.Error("Error getting RFQ address", "error", err)
return nil, fmt.Errorf("error getting RFQ address: %w", err)
}

// Build the quote
destAmount, err := m.getDestAmount(ctx, originAmount, chainID, destToken)
destAmount, err := m.getDestAmount(ctx, originAmount, input.DestChainID, destToken)
if err != nil {
logger.Error("Error getting dest amount", "error", err)
return nil, fmt.Errorf("error getting dest amount: %w", err)
}
quote = &model.PutQuoteRequest{
OriginChainID: origin,
OriginTokenAddr: originTokenAddr.Hex(),
DestChainID: chainID,
DestTokenAddr: address.Hex(),
OriginChainID: input.OriginChainID,
OriginTokenAddr: input.OriginTokenAddr.Hex(),
DestChainID: input.DestChainID,
DestTokenAddr: input.DestTokenAddr.Hex(),
DestAmount: destAmount.String(),
MaxOriginAmount: originAmount.String(),
FixedFee: fee.String(),
OriginFastBridgeAddress: originRFQAddr.Hex(),
DestFastBridgeAddress: destRFQAddr,
DestFastBridgeAddress: input.DestRFQAddr,
}
return quote, nil
}
Expand Down Expand Up @@ -471,12 +500,14 @@ func (m *Manager) recordQuoteAmounts(_ context.Context, observer metric.Observer
// getOriginAmount calculates the origin quote amount for a given route.
//
//nolint:cyclop
func (m *Manager) getOriginAmount(parentCtx context.Context, origin, dest int, address common.Address, balance *big.Int) (quoteAmount *big.Int, err error) {
func (m *Manager) getOriginAmount(parentCtx context.Context, input QuoteInput) (quoteAmount *big.Int, err error) {
ctx, span := m.metricsHandler.Tracer().Start(parentCtx, "getOriginAmount", trace.WithAttributes(
attribute.String(metrics.Origin, strconv.Itoa(origin)),
attribute.String(metrics.Destination, strconv.Itoa(dest)),
attribute.String("address", address.String()),
attribute.String("balance", balance.String()),
attribute.Int(metrics.Origin, input.OriginChainID),
attribute.Int(metrics.Destination, input.DestChainID),
attribute.String("dest_address", input.DestTokenAddr.String()),
attribute.String("origin_address", input.OriginTokenAddr.String()),
attribute.String("origin_balance", input.OriginBalance.String()),
attribute.String("dest_balance", input.DestBalance.String()),
))

defer func() {
Expand All @@ -487,11 +518,11 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, origin, dest int, a
// First, check if we have enough gas to complete the a bridge for this route
// If not, set the quote amount to zero to make sure a stale quote won't be used
// TODO: handle in-flight gas; for now we can set a high min_gas_token
sufficentGasOrigin, err := m.inventoryManager.HasSufficientGas(ctx, origin, nil)
sufficentGasOrigin, err := m.inventoryManager.HasSufficientGas(ctx, input.OriginChainID, nil)
if err != nil {
return nil, fmt.Errorf("error checking sufficient gas: %w", err)
}
sufficentGasDest, err := m.inventoryManager.HasSufficientGas(ctx, dest, nil)
sufficentGasDest, err := m.inventoryManager.HasSufficientGas(ctx, input.DestChainID, nil)
if err != nil {
return nil, fmt.Errorf("error checking sufficient gas: %w", err)
}
Expand All @@ -504,26 +535,26 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, origin, dest int, a
}

// Apply the quotePct
quotePct, err := m.config.GetQuotePct(dest)
quotePct, err := m.config.GetQuotePct(input.DestChainID)
if err != nil {
return nil, fmt.Errorf("error getting quote pct: %w", err)
}
balanceFlt := new(big.Float).SetInt(balance)
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(dest), address.Hex())
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(origin, tokenName, true)
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(dest, address)
minQuoteAmount := m.config.GetMinQuoteAmount(input.DestChainID, input.DestTokenAddr)
if quoteAmount.Cmp(minQuoteAmount) < 0 {
span.AddEvent("quote amount less than min quote amount", trace.WithAttributes(
attribute.String("quote_amount", quoteAmount.String()),
Expand All @@ -532,17 +563,39 @@ func (m *Manager) getOriginAmount(parentCtx context.Context, origin, dest int, a
quoteAmount = minQuoteAmount
}

// Finally, clip the quoteAmount by the balance
if quoteAmount.Cmp(balance) > 0 {
span.AddEvent("quote amount greater than balance", trace.WithAttributes(
// Clip the quoteAmount by the max origin balance
maxBalance := m.config.GetMaxBalance(input.OriginChainID, input.OriginTokenAddr)
if maxBalance != nil && input.OriginBalance != nil {
quotableBalance := new(big.Int).Sub(maxBalance, input.OriginBalance)
if quotableBalance.Cmp(big.NewInt(0)) <= 0 {
span.AddEvent("non-positive quotable balance", trace.WithAttributes(
attribute.String("quotable_balance", quotableBalance.String()),
attribute.String("max_balance", maxBalance.String()),
attribute.String("origin_balance", input.OriginBalance.String()),
))
quoteAmount = big.NewInt(0)
} else if quoteAmount.Cmp(quotableBalance) > 0 {
span.AddEvent("quote amount greater than quotable balance", trace.WithAttributes(
attribute.String("quote_amount", quoteAmount.String()),
attribute.String("quotable_balance", quotableBalance.String()),
attribute.String("max_balance", maxBalance.String()),
attribute.String("origin_balance", input.OriginBalance.String()),
))
quoteAmount = quotableBalance
}
}

// Finally, 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()),
attribute.String("balance", balance.String()),
attribute.String("balance", input.DestBalance.String()),
))
quoteAmount = balance
quoteAmount = input.DestBalance
}

// Deduct gas cost from the quote amount, if necessary
quoteAmount, err = m.deductGasCost(ctx, quoteAmount, address, dest)
quoteAmount, err = m.deductGasCost(ctx, quoteAmount, input.DestTokenAddr, input.DestChainID)
if err != nil {
return nil, fmt.Errorf("error deducting gas cost: %w", err)
}
Expand Down
54 changes: 37 additions & 17 deletions services/rfq/relayer/quoter/quoter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import (
func (s *QuoterSuite) TestGenerateQuotes() {
// Generate quotes for USDC on the destination chain.
balance := big.NewInt(1000_000_000) // 1000 USDC
quotes, err := s.manager.GenerateQuotes(s.GetTestContext(), int(s.destination), common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), balance)
inv := map[int]map[common.Address]*big.Int{}
quotes, err := s.manager.GenerateQuotes(s.GetTestContext(), int(s.destination), common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85"), balance, inv)
s.Require().NoError(err)

// Verify the quotes are generated as expected.
Expand All @@ -45,7 +46,8 @@ func (s *QuoterSuite) TestGenerateQuotes() {
func (s *QuoterSuite) TestGenerateQuotesForNativeToken() {
// Generate quotes for ETH on the destination chain.
balance, _ := new(big.Int).SetString("1000000000000000000", 10) // 1 ETH
quotes, err := s.manager.GenerateQuotes(s.GetTestContext(), int(s.destinationEth), util.EthAddress, balance)
inv := map[int]map[common.Address]*big.Int{}
quotes, err := s.manager.GenerateQuotes(s.GetTestContext(), int(s.destinationEth), util.EthAddress, balance, inv)
s.Require().NoError(err)

minGasToken, err := s.config.GetMinGasToken(int(s.destination))
Expand All @@ -72,7 +74,7 @@ func (s *QuoterSuite) TestGenerateQuotesForNativeToken() {
s.config.BaseChainConfig.MinGasToken = "100000000000000000" // 0.1 ETH
s.manager.SetConfig(s.config)

quotes, err = s.manager.GenerateQuotes(s.GetTestContext(), int(s.destinationEth), util.EthAddress, balance)
quotes, err = s.manager.GenerateQuotes(s.GetTestContext(), int(s.destinationEth), util.EthAddress, balance, inv)
s.Require().NoError(err)

minGasToken, err = s.config.GetMinGasToken(int(s.destination))
Expand All @@ -99,7 +101,7 @@ func (s *QuoterSuite) TestGenerateQuotesForNativeToken() {
s.config.BaseChainConfig.MinGasToken = "1000000000000000001" // 0.1 ETH
s.manager.SetConfig(s.config)

quotes, err = s.manager.GenerateQuotes(s.GetTestContext(), int(s.destinationEth), util.EthAddress, balance)
quotes, err = s.manager.GenerateQuotes(s.GetTestContext(), int(s.destinationEth), util.EthAddress, balance, inv)
s.NoError(err)
s.Equal(quotes[0].DestAmount, "0")
s.Equal(quotes[0].MaxOriginAmount, "0")
Expand Down Expand Up @@ -168,63 +170,81 @@ func (s *QuoterSuite) TestGetOriginAmount() {
origin := int(s.origin)
dest := int(s.destination)
address := common.HexToAddress("0x0b2c639c533813f4aa9d7837caf62653d097ff85")
originAddr := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
balance := big.NewInt(1000_000_000) // 1000 USDC

setQuoteParams := func(quotePct, quoteOffset float64, minQuoteAmount string) {
setQuoteParams := func(quotePct, quoteOffset float64, minQuoteAmount, maxBalance string) {
s.config.BaseChainConfig.QuotePct = &quotePct
destTokenCfg := s.config.Chains[dest].Tokens["USDC"]
destTokenCfg.MinQuoteAmount = minQuoteAmount
originTokenCfg := s.config.Chains[origin].Tokens["USDC"]
originTokenCfg.QuoteOffsetBps = quoteOffset
originTokenCfg.MaxBalance = &maxBalance
s.config.Chains[dest].Tokens["USDC"] = destTokenCfg
s.config.Chains[origin].Tokens["USDC"] = originTokenCfg
s.manager.SetConfig(s.config)
}

input := quoter.QuoteInput{
OriginChainID: origin,
DestChainID: dest,
OriginTokenAddr: originAddr,
DestTokenAddr: address,
OriginBalance: balance,
DestBalance: balance,
}

// Set default quote params; should return the balance.
quoteAmount, err := s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
quoteAmount, err := s.manager.GetOriginAmount(s.GetTestContext(), input)
s.NoError(err)
expectedAmount := balance
s.Equal(expectedAmount, quoteAmount)

// Set QuotePct to 50 with MinQuoteAmount of 0; should be 50% of balance.
setQuoteParams(50, 0, "0")
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
setQuoteParams(50, 0, "0", "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")
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
setQuoteParams(50, -100, "0", "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")
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
setQuoteParams(25, 0, "500", "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")
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
setQuoteParams(25, 0, "500", "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")
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
setQuoteParams(25, 0, "1500", "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 25 with MinQuoteAmount of 1500 and MaxBalance of 1200; should be 200.
setQuoteParams(25, 0, "1500", "1200")
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input)
s.NoError(err)
expectedAmount = big.NewInt(200_000_000)
s.Equal(expectedAmount, quoteAmount)

// Toggle insufficient gas; should be 0.
s.setGasSufficiency(false)
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), origin, dest, address, balance)
quoteAmount, err = s.manager.GetOriginAmount(s.GetTestContext(), input)
s.NoError(err)
expectedAmount = big.NewInt(0)
s.Equal(expectedAmount, quoteAmount)
Expand Down
Loading

0 comments on commit 694626a

Please sign in to comment.