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 6 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
100 changes: 68 additions & 32 deletions services/rfq/relayer/inventory/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,19 +425,19 @@ func (i *inventoryManagerImpl) HasSufficientGas(parentCtx context.Context, chain
// 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())
if err != nil {
return fmt.Errorf("could not get rebalance method: %w", err)
return fmt.Errorf("could not get origin rebalance method: %w", err)
}
if method == relconfig.RebalanceMethodNone {
if methodOrigin == relconfig.RebalanceMethodNone {
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 +475,9 @@ func (i *inventoryManagerImpl) Rebalance(parentCtx context.Context, chainID int,
}

// execute the rebalance
manager, ok := i.rebalanceManagers[method]
manager, ok := i.rebalanceManagers[methodOrigin]
if !ok {
return fmt.Errorf("no rebalance manager for method: %s", method)
return fmt.Errorf("no rebalance manager for method: %s", methodOrigin)
Copy link
Contributor

Choose a reason for hiding this comment

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

Validate the existence of a rebalance manager before attempting to execute.

- manager, ok := i.rebalanceManagers[methodOrigin]
- if !ok {
-   return fmt.Errorf("no rebalance manager for method: %s", methodOrigin)
+ if manager, ok := i.rebalanceManagers[methodOrigin]; !ok {
+   return fmt.Errorf("no rebalance manager for method: %s", methodOrigin)
+ } else {
+   err = manager.Execute(ctx, rebalance)
+   if err != nil {
+     return fmt.Errorf("could not execute rebalance: %w", err)
+   }
+ }

This change ensures that the method execution is only attempted if the manager exists, reducing the potential for runtime errors.


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
manager, ok := i.rebalanceManagers[methodOrigin]
if !ok {
return fmt.Errorf("no rebalance manager for method: %s", method)
return fmt.Errorf("no rebalance manager for method: %s", methodOrigin)
if manager, ok := i.rebalanceManagers[methodOrigin]; !ok {
return fmt.Errorf("no rebalance manager for method: %s", methodOrigin)
} else {
err = manager.Execute(ctx, rebalance)
if err != nil {
return fmt.Errorf("could not execute rebalance: %w", err)
}
}

}
err = manager.Execute(ctx, rebalance)
if err != nil {
Expand Down Expand Up @@ -522,54 +522,89 @@ func getRebalance(span trace.Span, cfg relconfig.Config, tokens map[int]map[comm
}
}

// 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),
)
}
}()
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

}

// validate the rebalance method pair
methodOrigin, err := cfg.GetRebalanceMethod(int(originTokenData.ChainID), originTokenData.Addr.Hex())
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove unnecessary conversions in GetRebalanceMethod calls.

- methodOrigin, err := cfg.GetRebalanceMethod(int(originTokenData.ChainID), originTokenData.Addr.Hex())
+ methodOrigin, err := cfg.GetRebalanceMethod(originTokenData.ChainID, originTokenData.Addr.Hex())
- methodDest, err := cfg.GetRebalanceMethod(int(destTokenData.ChainID), destTokenData.Addr.Hex())
+ methodDest, err := cfg.GetRebalanceMethod(destTokenData.ChainID, destTokenData.Addr.Hex())

Also applies to: 559-559


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
methodOrigin, err := cfg.GetRebalanceMethod(int(originTokenData.ChainID), originTokenData.Addr.Hex())
methodOrigin, err := cfg.GetRebalanceMethod(originTokenData.ChainID, originTokenData.Addr.Hex())

if err != nil {
return nil, fmt.Errorf("could not get origin rebalance method: %w", err)
}
methodDest, err := cfg.GetRebalanceMethod(int(destTokenData.ChainID), destTokenData.Addr.Hex())
if err != nil {
return nil, fmt.Errorf("could not get dest rebalance method: %w", err)
}
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)))
}
}()
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()))
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 +613,15 @@ func getRebalance(span trace.Span, cfg relconfig.Config, tokens map[int]map[comm
}

// 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 +634,10 @@ func getRebalance(span trace.Span, cfg relconfig.Config, tokens map[int]map[comm
}

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
43 changes: 41 additions & 2 deletions services/rfq/relayer/relconfig/enum.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package relconfig

import "fmt"

// RebalanceMethod is the method to rebalance.
//
//go:generate go run golang.org/x/tools/cmd/stringer -type=RebalanceMethod
type RebalanceMethod uint8

const (
Expand All @@ -15,3 +15,42 @@ const (
// RebalanceMethodNative is the rebalance method for native bridge.
RebalanceMethodNative
)

// RebalanceMethodFromString converts a string to a RebalanceMethod.
func RebalanceMethodFromString(str string) (RebalanceMethod, error) {
switch str {
case "synapsecctp":
return RebalanceMethodSynapseCCTP, nil
case "circlecctp":
return RebalanceMethodCircleCCTP, nil
case "native":
return RebalanceMethodNative, nil
case "":
return RebalanceMethodNone, nil
default:
return RebalanceMethodNone, fmt.Errorf("invalid rebalance method: %s", str)
}
}

func (i RebalanceMethod) String() string {
switch i {
case RebalanceMethodNone:
return ""
case RebalanceMethodSynapseCCTP:
return "synapsecctp"
case RebalanceMethodCircleCCTP:
return "circlecctp"
case RebalanceMethodNative:
return "native"
default:
return ""
}
}

// CoalesceRebalanceMethods coalesces two rebalance methods.
func CoalesceRebalanceMethods(a, b RebalanceMethod) RebalanceMethod {
if a == b {
return a
}
return RebalanceMethodNone
}
30 changes: 9 additions & 21 deletions services/rfq/relayer/relconfig/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,32 +402,20 @@ func (c Config) getTokenConfigByAddr(chainID int, tokenAddr string) (cfg TokenCo
return cfg, name, fmt.Errorf("no token config for chain %d and address %s", chainID, tokenAddr)
}

// GetRebalanceMethod returns the rebalance method for the given chain and token address.
// GetRebalanceMethod returns the rebalance method for the given chain path and token address.
// This method will error if there is a rebalance method mismatch, and neither methods correspond to
// RebalanceMethodNone.
func (c Config) GetRebalanceMethod(chainID int, tokenAddr string) (method RebalanceMethod, err error) {
tokenConfig, tokenName, err := c.getTokenConfigByAddr(chainID, tokenAddr)
tokenCfg, _, err := c.getTokenConfigByAddr(chainID, tokenAddr)
if err != nil {
return 0, err
}
if tokenConfig.RebalanceMethod == "" {
return RebalanceMethodNone, nil
}
for cid, chainCfg := range c.Chains {
tokenCfg, ok := chainCfg.Tokens[tokenName]
if ok {
if tokenConfig.RebalanceMethod != tokenCfg.RebalanceMethod {
return RebalanceMethodNone, fmt.Errorf("rebalance method mismatch for token %s on chains %d and %d", tokenName, chainID, cid)
}
}
}
switch tokenConfig.RebalanceMethod {
case "synapsecctp":
return RebalanceMethodSynapseCCTP, nil
case "circlecctp":
return RebalanceMethodCircleCCTP, nil
case "native":
return RebalanceMethodNative, nil

method, err = RebalanceMethodFromString(tokenCfg.RebalanceMethod)
if err != nil {
return 0, err
}
return RebalanceMethodNone, nil
return method, nil
}

// GetRebalanceMethods returns all rebalance methods present in the config.
Expand Down
Loading
Loading