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: rebalance edge cases & refactoring #2613

Merged
merged 14 commits into from
May 11, 2024
136 changes: 0 additions & 136 deletions services/rfq/relayer/inventory/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,142 +510,6 @@ func (i *inventoryManagerImpl) registerPendingRebalance(ctx context.Context, reb
return nil
}

//nolint:cyclop,gocognit,nilnil
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 {
return nil, fmt.Errorf("could not get maintenance pct: %w", err)
}

// get token metadata
var rebalanceTokenData *TokenMetadata
for address, tokenData := range tokens[chainID] {
if address == token {
rebalanceTokenData = tokenData
break
}
}

// 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 {
if destTokenData == nil || tokenData.Balance.Cmp(destTokenData.Balance) < 0 {
destTokenData = tokenData
}
if originTokenData == nil || tokenData.Balance.Cmp(originTokenData.Balance) > 0 {
originTokenData = 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
}

// 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)
}
methodDest, err := cfg.GetRebalanceMethod(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(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", 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 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(originTokenData.Balance, initialThresh)

// no need to rebalance since amount would not be positive
if amount.Cmp(big.NewInt(0)) <= 0 {
//nolint:nilnil
return nil, nil
}

// filter the rebalance amount by the configured min
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(originTokenData.ChainID, originTokenData.Addr)
if amount.Cmp(maxAmount) > 0 {
amount = maxAmount
}
if span != nil {
span.SetAttributes(
attribute.String("initial_thresh", initialThresh.String()),
attribute.String("rebalance_amount", amount.String()),
attribute.String("max_rebalance_amount", maxAmount.String()),
)
}

rebalance = &RebalanceData{
OriginMetadata: originTokenData,
DestMetadata: destTokenData,
Amount: amount,
Method: rebalanceMethod,
}
return rebalance, nil
}

func (i *inventoryManagerImpl) GetTokenMetadata(chainID int, token common.Address) (*TokenMetadata, error) {
i.mux.RLock()
defer i.mux.RUnlock()
Expand Down
209 changes: 151 additions & 58 deletions services/rfq/relayer/inventory/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,21 @@ func (i *InventoryTestSuite) TestGetRebalance() {
Decimals: 6,
ChainID: origin,
Addr: common.HexToAddress("0x0000000000000000000000000000000000000123"),
Balance: big.NewInt(0),
}
usdcDataDest := inventory.TokenMetadata{
Name: "USDC",
Decimals: 6,
ChainID: dest,
Addr: common.HexToAddress("0x0000000000000000000000000000000000000456"),
Balance: big.NewInt(0),
}
usdcDataExtra := inventory.TokenMetadata{
Name: "USDC",
Decimals: 6,
ChainID: extra,
Addr: common.HexToAddress("0x0000000000000000000000000000000000000789"),
Balance: big.NewInt(0),
}
tokens := map[int]map[common.Address]*inventory.TokenMetadata{
origin: {
Expand All @@ -88,6 +91,17 @@ func (i *InventoryTestSuite) TestGetRebalance() {
usdcDataDest.Addr: &usdcDataDest,
},
}
tokensWithExtra := map[int]map[common.Address]*inventory.TokenMetadata{
origin: {
usdcDataOrigin.Addr: &usdcDataOrigin,
},
dest: {
usdcDataDest.Addr: &usdcDataDest,
},
extra: {
usdcDataExtra.Addr: &usdcDataExtra,
},
}
getConfig := func(minRebalanceAmount, maxRebalanceAmount string, originMethod, destMethod relconfig.RebalanceMethod) relconfig.Config {
return relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
Expand Down Expand Up @@ -134,71 +148,150 @@ func (i *InventoryTestSuite) TestGetRebalance() {
}
}

// 10 USDC on both chains; no rebalance needed
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)
i.NoError(err)
i.Nil(rebalance)
i.Run("EqualBalances", func() {
// 10 USDC on both chains; no rebalance needed
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)
i.NoError(err)
i.Nil(rebalance)
})

// Set balances to zero
usdcDataOrigin.Balance = big.NewInt(0)
usdcDataDest.Balance = big.NewInt(0)
rebalance, err = inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
i.Nil(rebalance)
i.Run("ZeroBalances", func() {
// Set balances to zero
cfg := getConfig("", "", relconfig.RebalanceMethodSynapseCCTP, relconfig.RebalanceMethodSynapseCCTP)
usdcDataOrigin.Balance = big.NewInt(0)
usdcDataDest.Balance = big.NewInt(0)
rebalance, err := inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
i.Nil(rebalance)
})

// Set origin balance below maintenance threshold; need rebalance
usdcDataOrigin.Balance = big.NewInt(9e6)
usdcDataDest.Balance = big.NewInt(1e6)
rebalance, err = inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
expected := &inventory.RebalanceData{
OriginMetadata: &usdcDataOrigin,
DestMetadata: &usdcDataDest,
Amount: big.NewInt(4e6),
Method: relconfig.RebalanceMethodSynapseCCTP,
}
i.Equal(expected, rebalance)
i.Run("BasicRebalance", func() {
// Set dest balance below maintenance threshold; need rebalance
cfg := getConfig("", "", relconfig.RebalanceMethodSynapseCCTP, relconfig.RebalanceMethodSynapseCCTP)
usdcDataOrigin.Balance = big.NewInt(9e6)
usdcDataDest.Balance = big.NewInt(1e6)
rebalance, err := inventory.GetRebalance(cfg, tokens, dest, usdcDataDest.Addr)
i.NoError(err)
expected := &inventory.RebalanceData{
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)
i.Run("RebalanceMethodMismatch", func() {
// Set rebalance methods to mismatch
cfg := getConfig("", "", relconfig.RebalanceMethodCircleCCTP, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err := inventory.GetRebalance(cfg, tokens, dest, usdcDataDest.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)
i.Run("OneRebalanceMethodNone", func() {
// Set one rebalance method to None
cfg := getConfig("", "", relconfig.RebalanceMethodNone, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err := inventory.GetRebalance(cfg, tokens, dest, usdcDataDest.Addr)
i.NoError(err)
i.Nil(rebalance)
})

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

// Set max rebalance amount
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)
i.Run("AboveMaxRebalanceAmount", func() {
// Set max rebalance amount
cfgWithMax := getConfig("0", "1.1", relconfig.RebalanceMethodSynapseCCTP, relconfig.RebalanceMethodSynapseCCTP)
rebalance, err := inventory.GetRebalance(cfgWithMax, tokens, dest, usdcDataDest.Addr)
i.NoError(err)
expected := &inventory.RebalanceData{
OriginMetadata: &usdcDataOrigin,
DestMetadata: &usdcDataDest,
Amount: big.NewInt(1.1e6),
Method: relconfig.RebalanceMethodSynapseCCTP,
}
i.Equal(expected, rebalance)
})

// Increase initial threshold so that no rebalance can occur from origin
usdcDataOrigin.Balance = big.NewInt(2e6)
usdcDataDest.Balance = big.NewInt(1e6)
usdcDataExtra.Balance = big.NewInt(7e6)
rebalance, err = inventory.GetRebalance(cfg, tokens, origin, usdcDataOrigin.Addr)
i.NoError(err)
i.Nil(rebalance)
i.Run("BelowInitalThresholdOnOrigin", func() {
// Increase initial threshold so that no rebalance can occur from origin
cfg := getConfig("", "", relconfig.RebalanceMethodNone, relconfig.RebalanceMethodSynapseCCTP)
usdcDataOrigin.Balance = big.NewInt(2e6)
usdcDataDest.Balance = big.NewInt(1e6)
usdcDataExtra.Balance = big.NewInt(7e6)
rebalance, err := inventory.GetRebalance(cfg, tokens, dest, usdcDataDest.Addr)
i.NoError(err)
i.Nil(rebalance)
})

i.Run("SkipLowestBalanceWithMismatch", func() {
// Set origin as lowest balance, but mismatched rebalance method, so next lowest balance
// should be chosen
cfg := relconfig.Config{
Chains: map[int]relconfig.ChainConfig{
origin: {
Tokens: map[string]relconfig.TokenConfig{
"USDC": {
Address: usdcDataOrigin.Addr.Hex(),
Decimals: 6,
MaintenanceBalancePct: 20,
InitialBalancePct: 40,
MinRebalanceAmount: "",
MaxRebalanceAmount: "",
RebalanceMethod: "synapsecctp",
},
},
},
dest: {
Tokens: map[string]relconfig.TokenConfig{
"USDC": {
Address: usdcDataDest.Addr.Hex(),
Decimals: 6,
MaintenanceBalancePct: 20,
InitialBalancePct: 40,
MinRebalanceAmount: "",
MaxRebalanceAmount: "",
RebalanceMethod: "circlecctp",
},
},
},
extra: {
Tokens: map[string]relconfig.TokenConfig{
"USDC": {
Address: usdcDataExtra.Addr.Hex(),
Decimals: 6,
MaintenanceBalancePct: 20,
InitialBalancePct: 40,
MinRebalanceAmount: "",
MaxRebalanceAmount: "",
RebalanceMethod: "circlecctp",
},
},
},
},
}
usdcDataOrigin.Balance = big.NewInt(0)
usdcDataDest.Balance = big.NewInt(1e6)
usdcDataExtra.Balance = big.NewInt(9e6)
rebalance, err := inventory.GetRebalance(cfg, tokensWithExtra, dest, usdcDataDest.Addr)
i.NoError(err)
expected := &inventory.RebalanceData{
OriginMetadata: &usdcDataExtra,
DestMetadata: &usdcDataDest,
Amount: big.NewInt(5e6),
Method: relconfig.RebalanceMethodCircleCCTP,
}
i.Equal(expected, rebalance)
})
}

func (i *InventoryTestSuite) TestHasSufficientGas() {
Expand Down
Loading
Loading