Skip to content

Commit

Permalink
RFQ: rebalance edge cases & refactoring (#2613)
Browse files Browse the repository at this point in the history
* Fix: filter rebalance origin / dest on rebalance method

* Cleanup: move getRebalance() into rebalance.go

* Cleanup: add helpers to getRebalance()

* Cleanup: comments

* Feat: remove Rebalance() call upon deposit claimed

* Feat: getRebalance() takes in dest chain id

* Cleanup: comments

* Feat: extra check that we don't exceed origin maintenance

* Feat: add new test case for mismatched methods but existing rebalance

* Feat: break down TestGetRebalance into sub tests

* Cleanup: lint

* [goreleaser]

* [goreleaser]
  • Loading branch information
dwasse authored May 11, 2024
1 parent 537755e commit 4aa97e4
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 209 deletions.
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

0 comments on commit 4aa97e4

Please sign in to comment.