Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFQ: support multiple rebalance methods #2556

Merged
merged 20 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 74 additions & 34 deletions services/rfq/relayer/inventory/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"context"
"errors"
"fmt"
"math/big"
"strconv"
Expand Down Expand Up @@ -82,6 +83,9 @@
pendingHist metric.Float64Histogram
}

// ErrUnsupportedChain is the error for an unsupported chain.
var ErrUnsupportedChain = errors.New("could not get gas balance for unsupported chain")

// GetCommittableBalance gets the committable balances.
func (i *inventoryManagerImpl) GetCommittableBalance(ctx context.Context, chainID int, token common.Address, options ...BalanceFetchArgOption) (*big.Int, error) {
committableBalances, err := i.GetCommittableBalances(ctx, options...)
Expand All @@ -94,7 +98,7 @@
if balance == nil && token == chain.EthAddress {
gasBalance, ok := i.gasBalances[chainID]
if !ok || gasBalance == nil {
return nil, fmt.Errorf("could not get gas balance for chain %d", chainID)
return nil, ErrUnsupportedChain

Check warning on line 101 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L101

Added line #L101 was not covered by tests
}
balance = i.gasBalances[chainID]
}
Expand Down Expand Up @@ -425,19 +429,19 @@
// will be rebalanced.
//
//nolint:cyclop
func (i *inventoryManagerImpl) Rebalance(parentCtx context.Context, chainID int, token common.Address) error {
// evaluate the rebalance method
method, err := i.cfg.GetRebalanceMethod(chainID, token.Hex())
func (i *inventoryManagerImpl) Rebalance(parentCtx context.Context, chainID int, token common.Address) (err error) {
// short circuit if origin does not specify a rebalance method
methodOrigin, err := i.cfg.GetRebalanceMethod(chainID, token.Hex())

Check warning on line 434 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L432-L434

Added lines #L432 - L434 were not covered by tests
if err != nil {
return fmt.Errorf("could not get rebalance method: %w", err)
return fmt.Errorf("could not get origin rebalance method: %w", err)

Check warning on line 436 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L436

Added line #L436 was not covered by tests
}
if method == relconfig.RebalanceMethodNone {
if methodOrigin == relconfig.RebalanceMethodNone {

Check warning on line 438 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L438

Added line #L438 was not covered by tests
return nil
}

ctx, span := i.handler.Tracer().Start(parentCtx, "Rebalance", trace.WithAttributes(
attribute.Int(metrics.ChainID, chainID),
attribute.String("token", token.Hex()),
attribute.String("rebalance_method", method.String()),
))
defer func(err error) {
metrics.EndSpanWithErr(span, err)
Expand Down Expand Up @@ -475,9 +479,9 @@
}

// execute the rebalance
manager, ok := i.rebalanceManagers[method]
manager, ok := i.rebalanceManagers[rebalance.Method]

Check warning on line 482 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L482

Added line #L482 was not covered by tests
if !ok {
return fmt.Errorf("no rebalance manager for method: %s", method)
return fmt.Errorf("no rebalance manager for method: %s", methodOrigin)

Check warning on line 484 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L484

Added line #L484 was not covered by tests
}
err = manager.Execute(ctx, rebalance)
if err != nil {
Expand Down Expand Up @@ -506,7 +510,7 @@
return nil
}

//nolint:cyclop,gocognit
//nolint:cyclop,gocognit,nilnil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor the getRebalance function to reduce complexity.

Consider breaking down this function into smaller, more manageable functions. This could improve readability and maintainability, especially as the function handles multiple responsibilities such as fetching token metadata, determining rebalance origin and destination, and calculating rebalance amounts.

func getRebalance(span trace.Span, cfg relconfig.Config, tokens map[int]map[common.Address]*TokenMetadata, chainID int, token common.Address) (rebalance *RebalanceData, err error) {
maintenancePct, err := cfg.GetMaintenanceBalancePct(chainID, token.Hex())
if err != nil {
Expand All @@ -522,54 +526,89 @@
}
}

// get total balance for given token across all chains
totalBalance := big.NewInt(0)
// evaluate the origin and dest of the rebalance based on min/max token balances
var destTokenData, originTokenData *TokenMetadata
for _, tokenMap := range tokens {
for _, tokenData := range tokenMap {
if tokenData.Name == rebalanceTokenData.Name {
totalBalance.Add(totalBalance, tokenData.Balance)
if destTokenData == nil || tokenData.Balance.Cmp(destTokenData.Balance) < 0 {
destTokenData = tokenData
}
if originTokenData == nil || tokenData.Balance.Cmp(originTokenData.Balance) > 0 {
originTokenData = tokenData
}
Comment on lines +529 to +539
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize the logic for determining the rebalance origin and destination.

Consider using a more efficient method to determine the minimum and maximum balances, such as maintaining sorted lists of balances or using priority queues. This could significantly reduce the computational complexity, especially if the number of tokens is large.

}
}
}

// check if any balances are below maintenance threshold
var minTokenData, maxTokenData *TokenMetadata
for _, tokenMap := range tokens {
for _, tokenData := range tokenMap {
if tokenData.Name == rebalanceTokenData.Name {
if minTokenData == nil || tokenData.Balance.Cmp(minTokenData.Balance) < 0 {
minTokenData = tokenData
}
if maxTokenData == nil || tokenData.Balance.Cmp(maxTokenData.Balance) > 0 {
maxTokenData = tokenData
}
}
// if the given chain is not the origin of the rebalance, no need to do anything
defer func() {
if span != nil {
span.SetAttributes(
attribute.Int("rebalance_chain_id", chainID),
attribute.Int("rebalance_origin", originTokenData.ChainID),
attribute.Int("rebalance_dest", destTokenData.ChainID),
)
}

Check warning on line 552 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L547-L552

Added lines #L547 - L552 were not covered by tests
}()
if originTokenData.ChainID != chainID {
return nil, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a sentinel error instead of returning both nil error and invalid value.

- return nil, nil
+ return nil, ErrInvalidRebalance

Also applies to: 572-572


Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
return nil, nil
return nil, ErrInvalidRebalance

}

Check warning on line 556 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L555-L556

Added lines #L555 - L556 were not covered by tests

// validate the rebalance method pair
methodOrigin, err := cfg.GetRebalanceMethod(originTokenData.ChainID, originTokenData.Addr.Hex())
if err != nil {
return nil, fmt.Errorf("could not get origin rebalance method: %w", err)
}

Check warning on line 562 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L561-L562

Added lines #L561 - L562 were not covered by tests
methodDest, err := cfg.GetRebalanceMethod(destTokenData.ChainID, destTokenData.Addr.Hex())
if err != nil {
return nil, fmt.Errorf("could not get dest rebalance method: %w", err)
}

Check warning on line 566 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L565-L566

Added lines #L565 - L566 were not covered by tests
rebalanceMethod := relconfig.CoalesceRebalanceMethods(methodOrigin, methodDest)
defer func() {
if span != nil {
span.SetAttributes(attribute.Int("rebalance_method", int(rebalanceMethod)))
span.SetAttributes(attribute.Int("origin_rebalance_method", int(methodOrigin)))
span.SetAttributes(attribute.Int("dest_rebalance_method", int(methodDest)))

Check warning on line 572 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L570-L572

Added lines #L570 - L572 were not covered by tests
}
}()
if rebalanceMethod == relconfig.RebalanceMethodNone {
return nil, nil
}

// get the initialPct for the origin chain
initialPct, err := cfg.GetInitialBalancePct(maxTokenData.ChainID, maxTokenData.Addr.Hex())
initialPct, err := cfg.GetInitialBalancePct(originTokenData.ChainID, originTokenData.Addr.Hex())
if err != nil {
return nil, fmt.Errorf("could not get initial pct: %w", err)
}

// calculate maintenance threshold relative to total balance
totalBalance := big.NewInt(0)
for _, tokenMap := range tokens {
for _, tokenData := range tokenMap {
if tokenData.Name == rebalanceTokenData.Name {
totalBalance.Add(totalBalance, tokenData.Balance)
}
}
}
maintenanceThresh, _ := new(big.Float).Mul(new(big.Float).SetInt(totalBalance), big.NewFloat(maintenancePct/100)).Int(nil)
if span != nil {
span.SetAttributes(attribute.Float64("maintenance_pct", maintenancePct))
span.SetAttributes(attribute.Float64("initial_pct", initialPct))
span.SetAttributes(attribute.String("max_token_balance", maxTokenData.Balance.String()))
span.SetAttributes(attribute.String("min_token_balance", minTokenData.Balance.String()))
span.SetAttributes(attribute.String("max_token_balance", originTokenData.Balance.String()))
span.SetAttributes(attribute.String("min_token_balance", destTokenData.Balance.String()))

Check warning on line 599 in services/rfq/relayer/inventory/manager.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/inventory/manager.go#L598-L599

Added lines #L598 - L599 were not covered by tests
span.SetAttributes(attribute.String("total_balance", totalBalance.String()))
span.SetAttributes(attribute.String("maintenance_thresh", maintenanceThresh.String()))
}

// check if the minimum balance is below the threshold and trigger rebalance
if minTokenData.Balance.Cmp(maintenanceThresh) > 0 {
if destTokenData.Balance.Cmp(maintenanceThresh) > 0 {
return rebalance, nil
}

// calculate the amount to rebalance vs the initial threshold on origin
initialThresh, _ := new(big.Float).Mul(new(big.Float).SetInt(totalBalance), big.NewFloat(initialPct/100)).Int(nil)
amount := new(big.Int).Sub(maxTokenData.Balance, initialThresh)
amount := new(big.Int).Sub(originTokenData.Balance, initialThresh)

// no need to rebalance since amount would not be positive
if amount.Cmp(big.NewInt(0)) <= 0 {
Expand All @@ -578,15 +617,15 @@
}

// filter the rebalance amount by the configured min
minAmount := cfg.GetMinRebalanceAmount(maxTokenData.ChainID, maxTokenData.Addr)
minAmount := cfg.GetMinRebalanceAmount(originTokenData.ChainID, originTokenData.Addr)
if amount.Cmp(minAmount) < 0 {
// no need to rebalance
//nolint:nilnil
return nil, nil
}

// clip the rebalance amount by the configured max
maxAmount := cfg.GetMaxRebalanceAmount(maxTokenData.ChainID, maxTokenData.Addr)
maxAmount := cfg.GetMaxRebalanceAmount(originTokenData.ChainID, originTokenData.Addr)
if amount.Cmp(maxAmount) > 0 {
amount = maxAmount
}
Expand All @@ -599,9 +638,10 @@
}

rebalance = &RebalanceData{
OriginMetadata: maxTokenData,
DestMetadata: minTokenData,
OriginMetadata: originTokenData,
DestMetadata: destTokenData,
Amount: amount,
Method: rebalanceMethod,
}
return rebalance, nil
}
Expand Down
25 changes: 21 additions & 4 deletions services/rfq/relayer/inventory/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (i *InventoryTestSuite) TestGetRebalance() {
usdcDataDest.Addr: &usdcDataDest,
},
}
getConfig := func(minRebalanceAmount, maxRebalanceAmount string) relconfig.Config {
getConfig := func(minRebalanceAmount, maxRebalanceAmount string, originMethod, destMethod relconfig.RebalanceMethod) relconfig.Config {
return relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
origin: {
Expand All @@ -100,6 +100,7 @@ func (i *InventoryTestSuite) TestGetRebalance() {
InitialBalancePct: 50,
MinRebalanceAmount: minRebalanceAmount,
MaxRebalanceAmount: maxRebalanceAmount,
RebalanceMethod: originMethod.String(),
},
},
},
Expand All @@ -112,6 +113,7 @@ func (i *InventoryTestSuite) TestGetRebalance() {
InitialBalancePct: 50,
MinRebalanceAmount: minRebalanceAmount,
MaxRebalanceAmount: maxRebalanceAmount,
RebalanceMethod: destMethod.String(),
},
},
},
Expand All @@ -124,6 +126,7 @@ func (i *InventoryTestSuite) TestGetRebalance() {
InitialBalancePct: 0,
MinRebalanceAmount: minRebalanceAmount,
MaxRebalanceAmount: maxRebalanceAmount,
RebalanceMethod: destMethod.String(),
},
},
},
Expand All @@ -132,7 +135,7 @@ func (i *InventoryTestSuite) TestGetRebalance() {
}

// 10 USDC on both chains; no rebalance needed
cfg := getConfig("", "")
cfg := getConfig("", "", relconfig.RebalanceMethodSynapseCCTP, relconfig.RebalanceMethodSynapseCCTP)
usdcDataOrigin.Balance = big.NewInt(1e7)
usdcDataDest.Balance = big.NewInt(1e7)
rebalance, err := inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
Expand All @@ -155,23 +158,37 @@ func (i *InventoryTestSuite) TestGetRebalance() {
OriginMetadata: &usdcDataOrigin,
DestMetadata: &usdcDataDest,
Amount: big.NewInt(4e6),
Method: relconfig.RebalanceMethodSynapseCCTP,
}
i.Equal(expected, rebalance)

// Set rebalance methods to mismatch
cfg = getConfig("", "", relconfig.RebalanceMethodCircleCCTP, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err = inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
i.Nil(rebalance)

// Set one rebalance method to None
cfg = getConfig("", "", relconfig.RebalanceMethodNone, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err = inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
i.Nil(rebalance)

// Set min rebalance amount
cfgWithMax := getConfig("10", "1000000000")
cfgWithMax := getConfig("10", "1000000000", relconfig.RebalanceMethodSynapseCCTP, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err = inventory.GetRebalance(cfgWithMax, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
i.Nil(rebalance)

// Set max rebalance amount
cfgWithMax = getConfig("0", "1.1")
cfgWithMax = getConfig("0", "1.1", relconfig.RebalanceMethodSynapseCCTP, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err = inventory.GetRebalance(cfgWithMax, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
expected = &inventory.RebalanceData{
OriginMetadata: &usdcDataOrigin,
DestMetadata: &usdcDataDest,
Amount: big.NewInt(1.1e6),
Method: relconfig.RebalanceMethodSynapseCCTP,
}
i.Equal(expected, rebalance)

Expand Down
3 changes: 3 additions & 0 deletions services/rfq/relayer/inventory/rebalance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package inventory
import (
"context"
"math/big"

"github.com/synapsecns/sanguine/services/rfq/relayer/relconfig"
)

// RebalanceData contains metadata for a rebalance action.
type RebalanceData struct {
OriginMetadata *TokenMetadata
DestMetadata *TokenMetadata
Amount *big.Int
Method relconfig.RebalanceMethod
}

// RebalanceManager is the interface for the rebalance manager.
Expand Down
8 changes: 5 additions & 3 deletions services/rfq/relayer/pricer/fee_pricer.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,13 @@
if err != nil {
return nil, err
}
header, err := client.HeaderByNumber(ctx, nil)
gasPrice, err = client.SuggestGasPrice(ctx)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to suggest gas price on chain %d: %w", chainID, err)
}

Check warning on line 282 in services/rfq/relayer/pricer/fee_pricer.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/pricer/fee_pricer.go#L281-L282

Added lines #L281 - L282 were not covered by tests
if gasPrice == nil {
return nil, fmt.Errorf("gas price is nil on chain %d", chainID)

Check warning on line 284 in services/rfq/relayer/pricer/fee_pricer.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/pricer/fee_pricer.go#L284

Added line #L284 was not covered by tests
}
gasPrice = header.BaseFee
f.gasPriceCache.Set(chainID, gasPrice, 0)
} else {
gasPrice = gasPriceItem.Value()
Expand Down
Loading
Loading