diff --git a/core/services/ocr2/plugins/liquiditymanager/factory.go b/core/services/ocr2/plugins/liquiditymanager/factory.go index 13e7509c2f..754fcca9a8 100644 --- a/core/services/ocr2/plugins/liquiditymanager/factory.go +++ b/core/services/ocr2/plugins/liquiditymanager/factory.go @@ -67,7 +67,7 @@ func (p PluginFactory) buildRebalancer() (rebalalgo.RebalancingAlgo, error) { case models.RebalancerTypeMinLiquidity: return rebalalgo.NewMinLiquidityRebalancer(p.lggr), nil case models.RebalancerTypeTargetAndMin: - return rebalalgo.NewTargetMinBalancer(p.lggr), nil + return rebalalgo.NewTargetMinBalancer(p.lggr, p.config), nil default: return nil, fmt.Errorf("invalid rebalancer type %s", p.config.RebalancerConfig.Type) } diff --git a/core/services/ocr2/plugins/liquiditymanager/graph/graph.go b/core/services/ocr2/plugins/liquiditymanager/graph/graph.go index 7523a75cc9..fcea5e5957 100644 --- a/core/services/ocr2/plugins/liquiditymanager/graph/graph.go +++ b/core/services/ocr2/plugins/liquiditymanager/graph/graph.go @@ -14,6 +14,8 @@ type GraphWriter interface { Add(from, to Data) error // SetLiquidity sets the liquidity of the provided network. SetLiquidity(n models.NetworkSelector, liquidity *big.Int) bool + // SetTargetLiquidity sets the target liquidity of the provided network. + SetTargetLiquidity(n models.NetworkSelector, liquidity *big.Int) bool } // NodeReader provides read access to the data saved in the graph nodes. diff --git a/core/services/ocr2/plugins/liquiditymanager/graph/writer.go b/core/services/ocr2/plugins/liquiditymanager/graph/writer.go index 1d0ee45d59..8671087b12 100644 --- a/core/services/ocr2/plugins/liquiditymanager/graph/writer.go +++ b/core/services/ocr2/plugins/liquiditymanager/graph/writer.go @@ -41,6 +41,27 @@ func (g *liquidityGraph) SetLiquidity(n models.NetworkSelector, liquidity *big.I return true } +func (g *liquidityGraph) SetTargetLiquidity(n models.NetworkSelector, target *big.Int) bool { + g.lock.Lock() + defer g.lock.Unlock() + + if !g.hasNetwork(n) { + return false + } + + prev := g.data[n] + g.data[n] = Data{ + Liquidity: prev.Liquidity, + TokenAddress: prev.TokenAddress, + LiquidityManagerAddress: prev.LiquidityManagerAddress, + ConfigDigest: prev.ConfigDigest, + NetworkSelector: prev.NetworkSelector, + MinimumLiquidity: prev.MinimumLiquidity, + TargetLiquidity: target, + } + return true +} + func (g *liquidityGraph) AddNetwork(n models.NetworkSelector, data Data) bool { g.lock.Lock() defer g.lock.Unlock() diff --git a/core/services/ocr2/plugins/liquiditymanager/models/config.go b/core/services/ocr2/plugins/liquiditymanager/models/config.go index cf3249e4d1..aaa4258651 100644 --- a/core/services/ocr2/plugins/liquiditymanager/models/config.go +++ b/core/services/ocr2/plugins/liquiditymanager/models/config.go @@ -17,12 +17,10 @@ type PluginConfig struct { } type RebalancerConfig struct { - Type string `json:"type"` -} - -type NetworkTarget struct { - Network NetworkSelector `json:"network,string"` - Target *big.Int `json:"target"` + Type string `json:"type"` + DefaultTarget *big.Int `json:"defaultTarget"` + // NetworkTargetOverrides is a map of NetworkSelector to big Int amounts + NetworkTargetOverrides map[NetworkSelector]*big.Int `json:"networkTargetOverrides"` } func ValidateRebalancerConfig(config RebalancerConfig) error { @@ -47,6 +45,7 @@ var ( AllRebalancerTypes = []string{ RebalancerTypePingPong, RebalancerTypeMinLiquidity, + RebalancerTypeTargetAndMin, } ) diff --git a/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum.go b/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum.go index c479b0f657..b7e492f541 100644 --- a/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum.go +++ b/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum.go @@ -13,16 +13,18 @@ import ( // TargetMinBalancer tries to reach balance using a target and minimum liquidity that is configured on each network. type TargetMinBalancer struct { - lggr logger.Logger + lggr logger.Logger + config models.PluginConfig } -func NewTargetMinBalancer(lggr logger.Logger) *TargetMinBalancer { +func NewTargetMinBalancer(lggr logger.Logger, config models.PluginConfig) *TargetMinBalancer { return &TargetMinBalancer{ - lggr: lggr.With("service", "TargetMinBalancer"), + lggr: lggr.With("service", "TargetMinBalancer"), + config: config, } } -type determineTransfersFunc func(graphLater graph.Graph, targetNetwork models.NetworkSelector, networkFunds map[models.NetworkSelector]*Funds) ([]models.ProposedTransfer, error) +type determineTransfersFunc func(graphFuture graph.Graph, targetNetwork models.NetworkSelector, networkFunds map[models.NetworkSelector]*Funds) ([]models.ProposedTransfer, error) func (r *TargetMinBalancer) ComputeTransfersToBalance(graphNow graph.Graph, nonExecutedTransfers []UnexecutedTransfer) ([]models.ProposedTransfer, error) { nonExecutedTransfers = filterUnexecutedTransfers(nonExecutedTransfers) @@ -59,13 +61,13 @@ func (r *TargetMinBalancer) ComputeTransfersToBalance(graphNow graph.Graph, nonE func (r *TargetMinBalancer) rebalancingRound(graphNow graph.Graph, nonExecutedTransfers []UnexecutedTransfer, transfersFunc determineTransfersFunc) ([]models.ProposedTransfer, error) { var err error - graphLater, err := getExpectedGraph(graphNow, nonExecutedTransfers) + graphFuture, err := r.getExpectedGraph(graphNow, nonExecutedTransfers) if err != nil { return nil, fmt.Errorf("get expected graph: %w", err) } r.lggr.Debugf("finding networks that require funding") - networksRequiringFunding, networkFunds, err := r.findNetworksRequiringFunding(graphNow, graphLater) + networksRequiringFunding, networkFunds, err := r.findNetworksRequiringFunding(graphNow, graphFuture) if err != nil { return nil, fmt.Errorf("find networks that require funding: %w", err) } @@ -75,17 +77,17 @@ func (r *TargetMinBalancer) rebalancingRound(graphNow graph.Graph, nonExecutedTr proposedTransfers := make([]models.ProposedTransfer, 0) for _, net := range networksRequiringFunding { r.lggr.Debugf("finding transfers for network %v", net) - potentialTransfers, err1 := transfersFunc(graphLater, net, networkFunds) + potentialTransfers, err1 := transfersFunc(graphFuture, net, networkFunds) if err1 != nil { return nil, fmt.Errorf("finding transfers for network %v: %w", net, err1) } - dataLater, err2 := graphLater.GetData(net) + dataFuture, err2 := graphFuture.GetData(net) if err2 != nil { - return nil, fmt.Errorf("get data later of net %v: %w", net, err2) + return nil, fmt.Errorf("get future data of net %v: %w", net, err2) } - liqDiffLater := new(big.Int).Sub(dataLater.TargetLiquidity, dataLater.Liquidity) - netProposedTransfers, err3 := r.applyProposedTransfers(graphLater, potentialTransfers, liqDiffLater) + liqDiffFuture := new(big.Int).Sub(dataFuture.TargetLiquidity, dataFuture.Liquidity) + netProposedTransfers, err3 := r.applyProposedTransfers(graphFuture, potentialTransfers, liqDiffFuture) if err3 != nil { return nil, fmt.Errorf("applying transfers: %w", err3) } @@ -95,62 +97,101 @@ func (r *TargetMinBalancer) rebalancingRound(graphNow graph.Graph, nonExecutedTr return proposedTransfers, nil } -func (r *TargetMinBalancer) findNetworksRequiringFunding(graphNow, graphLater graph.Graph) ([]models.NetworkSelector, map[models.NetworkSelector]*Funds, error) { - mapNetworkFunds := make(map[models.NetworkSelector]*Funds) - liqDiffsLater := make(map[models.NetworkSelector]*big.Int) +// getExpectedGraph returns a copy of the graph instance with all the non-executed transfers applied and targets set for each network. +func (r *TargetMinBalancer) getExpectedGraph(g graph.Graph, nonExecutedTransfers []UnexecutedTransfer) (graph.Graph, error) { + expG := g.Clone() + + for _, tr := range nonExecutedTransfers { + liqTo, err := expG.GetLiquidity(tr.ToNetwork()) + if err != nil { + return nil, err + } + expG.SetLiquidity(tr.ToNetwork(), big.NewInt(0).Add(liqTo, tr.TransferAmount())) + + // we only subtract from the sender if the transfer is still in progress, otherwise the source value would have already been updated + switch tr.TransferStatus() { + case models.TransferStatusProposed, models.TransferStatusInflight: + liqFrom, err := expG.GetLiquidity(tr.FromNetwork()) + if err != nil { + return nil, err + } + expG.SetLiquidity(tr.FromNetwork(), big.NewInt(0).Sub(liqFrom, tr.TransferAmount())) + } + } - //TODO: LM-23 Create minTokenTransfer config to filter-out small rebalance txs - // check that the transfer is not tiny, we should only transfer if it is significant. What is too tiny? - // we could prevent this by only making a network requiring funding if its below X% of the target + for _, selector := range expG.GetNetworks() { + target := r.config.RebalancerConfig.DefaultTarget + override, ok := r.config.RebalancerConfig.NetworkTargetOverrides[selector] + if ok { + target = override + } + expG.SetTargetLiquidity(selector, target) + } + + return expG, nil +} + +func (r *TargetMinBalancer) findNetworksRequiringFunding(graphNow, graphFuture graph.Graph) ([]models.NetworkSelector, map[models.NetworkSelector]*Funds, error) { + mapNetworkFunds := make(map[models.NetworkSelector]*Funds) + liqDiffsFuture := make(map[models.NetworkSelector]*big.Int) res := make([]models.NetworkSelector, 0) for _, net := range graphNow.GetNetworks() { //use min here for transferable. because we don't know when the transfers will complete and want to avoid issues - transferableAmount, ataErr := availableTransferableAmount(graphNow, graphLater, net) + transferableAmount, ataErr := availableTransferableAmount(graphNow, graphFuture, net) if ataErr != nil { return nil, nil, fmt.Errorf("getting available transferrable amount for net %d: %v", net, ataErr) } - dataLater, err := graphLater.GetData(net) + dataFuture, err := graphFuture.GetData(net) if err != nil { - return nil, nil, fmt.Errorf("get data later of net %v: %w", net, err) + return nil, nil, fmt.Errorf("get future data of net %v: %w", net, err) } - liqDiffLater := new(big.Int).Sub(dataLater.TargetLiquidity, dataLater.Liquidity) + liqDiffFuture := new(big.Int).Sub(dataFuture.TargetLiquidity, dataFuture.Liquidity) mapNetworkFunds[net] = &Funds{ AvailableAmount: transferableAmount, } - if liqDiffLater.Cmp(big.NewInt(0)) <= 0 { + if liqDiffFuture.Cmp(big.NewInt(0)) <= 0 { + continue + } + + // only if we are below 5% else it's too little to worry about now + fivePercent := big.NewInt(5) + hundred := big.NewInt(100) + value := new(big.Int).Mul(dataFuture.TargetLiquidity, fivePercent) + value.Div(value, hundred) + if liqDiffFuture.Cmp(value) < 0 { continue } - liqDiffsLater[net] = liqDiffLater + liqDiffsFuture[net] = liqDiffFuture res = append(res, net) } - sort.Slice(res, func(i, j int) bool { return liqDiffsLater[res[i]].Cmp(liqDiffsLater[res[j]]) > 0 }) + sort.Slice(res, func(i, j int) bool { return liqDiffsFuture[res[i]].Cmp(liqDiffsFuture[res[j]]) > 0 }) return res, mapNetworkFunds, nil } -func (r *TargetMinBalancer) oneHopTransfers(graphLater graph.Graph, targetNetwork models.NetworkSelector, networkFunds map[models.NetworkSelector]*Funds) ([]models.ProposedTransfer, error) { +func (r *TargetMinBalancer) oneHopTransfers(graphFuture graph.Graph, targetNetwork models.NetworkSelector, networkFunds map[models.NetworkSelector]*Funds) ([]models.ProposedTransfer, error) { zero := big.NewInt(0) potentialTransfers := make([]models.ProposedTransfer, 0) - neighbors, exist := graphLater.GetNeighbors(targetNetwork, false) + neighbors, exist := graphFuture.GetNeighbors(targetNetwork, false) if !exist { r.lggr.Debugf("no neighbors found for %v", targetNetwork) return nil, nil } - targetLater, err := graphLater.GetData(targetNetwork) + targetFuture, err := graphFuture.GetData(targetNetwork) if err != nil { - return nil, fmt.Errorf("get data later of net %v: %w", targetNetwork, err) + return nil, fmt.Errorf("get data Future of net %v: %w", targetNetwork, err) } for _, source := range neighbors { - transferAmount := new(big.Int).Sub(targetLater.TargetLiquidity, targetLater.Liquidity) + transferAmount := new(big.Int).Sub(targetFuture.TargetLiquidity, targetFuture.Liquidity) r.lggr.Debugf("checking transfer from %v to %v for amount %v", source, targetNetwork, transferAmount) //source network available transferable amount - srcData, dErr := graphLater.GetData(source) + srcData, dErr := graphFuture.GetData(source) if dErr != nil { - return nil, fmt.Errorf("error during GetData for %v in graphLater: %v", source, dErr) + return nil, fmt.Errorf("error during GetData for %v in graphFuture: %v", source, dErr) } srcAvailableAmount := new(big.Int).Sub(srcData.Liquidity, srcData.MinimumLiquidity) srcAmountToTarget := new(big.Int).Sub(srcData.Liquidity, srcData.TargetLiquidity) @@ -180,40 +221,40 @@ func (r *TargetMinBalancer) oneHopTransfers(graphLater graph.Graph, targetNetwor } // twoHopTransfers finds networks that can increase liquidity of the target network with an intermediate network. -func (r *TargetMinBalancer) twoHopTransfers(graphLater graph.Graph, targetNetwork models.NetworkSelector, networkFunds map[models.NetworkSelector]*Funds) ([]models.ProposedTransfer, error) { +func (r *TargetMinBalancer) twoHopTransfers(graphFuture graph.Graph, targetNetwork models.NetworkSelector, networkFunds map[models.NetworkSelector]*Funds) ([]models.ProposedTransfer, error) { zero := big.NewInt(0) iterator := func(nodes ...graph.Data) bool { return true } potentialTransfers := make([]models.ProposedTransfer, 0) - for _, source := range graphLater.GetNetworks() { + for _, source := range graphFuture.GetNetworks() { if source == targetNetwork { continue } - path := graphLater.FindPath(source, targetNetwork, 2, iterator) + path := graphFuture.FindPath(source, targetNetwork, 2, iterator) if len(path) != 2 { continue } middle := path[0] - targetData, err := graphLater.GetData(targetNetwork) + targetData, err := graphFuture.GetData(targetNetwork) if err != nil { - return nil, fmt.Errorf("error during GetData for %v in graphLater: %v", targetNetwork, err) + return nil, fmt.Errorf("error during GetData for %v in graphFuture: %v", targetNetwork, err) } transferAmount := new(big.Int).Sub(targetData.TargetLiquidity, targetData.Liquidity) r.lggr.Debugf("checking transfer from %v -> %v -> %v for amount %v", source, middle, targetNetwork, transferAmount) //source network available transferable amount - srcData, dErr := graphLater.GetData(source) + srcData, dErr := graphFuture.GetData(source) if dErr != nil { - return nil, fmt.Errorf("error during GetData for %v in graphLater: %v", source, dErr) + return nil, fmt.Errorf("error during GetData for %v in graphFuture: %v", source, dErr) } srcAvailableAmount := new(big.Int).Sub(srcData.Liquidity, srcData.MinimumLiquidity) srcAmountToTarget := new(big.Int).Sub(srcData.Liquidity, srcData.TargetLiquidity) //middle network available transferable amount - middleData, dErr := graphLater.GetData(middle) + middleData, dErr := graphFuture.GetData(middle) if dErr != nil { - return nil, fmt.Errorf("error during GetData for %v in graphLater: %v", middle, dErr) + return nil, fmt.Errorf("error during GetData for %v in graphFuture: %v", middle, dErr) } middleAvailableAmount := new(big.Int).Sub(middleData.Liquidity, middleData.MinimumLiquidity) middleAmountToTarget := new(big.Int).Sub(middleData.Liquidity, middleData.TargetLiquidity) @@ -251,7 +292,7 @@ func (r *TargetMinBalancer) twoHopTransfers(graphLater graph.Graph, targetNetwor // applyProposedTransfers applies the proposed transfers to the graph. // increments the raised funds and gives a refund to the sender if more funds have been raised than the required amount. // It updates the liquidity of the sender and receiver networks in the graph. It stops further transfers if all funds have been raised. -func (r *TargetMinBalancer) applyProposedTransfers(graphLater graph.Graph, potentialTransfers []models.ProposedTransfer, requiredAmount *big.Int) ([]models.ProposedTransfer, error) { +func (r *TargetMinBalancer) applyProposedTransfers(graphFuture graph.Graph, potentialTransfers []models.ProposedTransfer, requiredAmount *big.Int) ([]models.ProposedTransfer, error) { // sort by amount,sender,receiver sort.Slice(potentialTransfers, func(i, j int) bool { if potentialTransfers[i].Amount.Cmp(potentialTransfers[j].Amount) == 0 { @@ -272,7 +313,7 @@ func (r *TargetMinBalancer) applyProposedTransfers(graphLater graph.Graph, poten continue } - senderData, err := graphLater.GetData(d.From) + senderData, err := graphFuture.GetData(d.From) if err != nil { return nil, fmt.Errorf("get liquidity of sender %v: %w", d.From, err) } @@ -298,17 +339,17 @@ func (r *TargetMinBalancer) applyProposedTransfers(graphLater graph.Graph, poten r.lggr.Debugf("applying transfer: %v", d) proposedTransfers = append(proposedTransfers, models.ProposedTransfer{From: d.From, To: d.To, Amount: d.Amount}) - liqBefore, err := graphLater.GetLiquidity(d.To) + liqBefore, err := graphFuture.GetLiquidity(d.To) if err != nil { return nil, fmt.Errorf("get liquidity of transfer receiver %v: %w", d.To, err) } - graphLater.SetLiquidity(d.To, big.NewInt(0).Add(liqBefore, d.Amount.ToInt())) + graphFuture.SetLiquidity(d.To, big.NewInt(0).Add(liqBefore, d.Amount.ToInt())) - liqBefore, err = graphLater.GetLiquidity(d.From) + liqBefore, err = graphFuture.GetLiquidity(d.From) if err != nil { return nil, fmt.Errorf("get liquidity of sender %v: %w", d.From, err) } - graphLater.SetLiquidity(d.From, big.NewInt(0).Sub(liqBefore, d.Amount.ToInt())) + graphFuture.SetLiquidity(d.From, big.NewInt(0).Sub(liqBefore, d.Amount.ToInt())) if fundsRaised.Cmp(requiredAmount) >= 0 { r.lggr.Debugf("all funds raised skipping further transfers") diff --git a/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum_test.go b/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum_test.go index 150fad455b..77e0f90d30 100644 --- a/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum_test.go +++ b/core/services/ocr2/plugins/liquiditymanager/rebalalgo/target_balance_with_minimum_test.go @@ -226,21 +226,30 @@ func TestTargetMinBalancer_ComputeTransfersToBalance_arb_eth_opt_0(t *testing.T) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + overrides := make(map[models.NetworkSelector]*big.Int) g := graph.NewGraph() for net, b := range tc.balances { g.(graph.GraphTest).AddNetwork(net, graph.Data{ Liquidity: big.NewInt(b), NetworkSelector: net, MinimumLiquidity: big.NewInt(tc.minimums[net]), - TargetLiquidity: big.NewInt(tc.targets[net]), }) + overrides[net] = big.NewInt(tc.targets[net]) } assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, arb)) assert.NoError(t, g.(graph.GraphTest).AddConnection(arb, eth)) assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, opt)) assert.NoError(t, g.(graph.GraphTest).AddConnection(opt, eth)) - r := NewTargetMinBalancer(lggr) + pluginConfigOverrides := models.PluginConfig{ + RebalancerConfig: models.RebalancerConfig{ + Type: "target-and-min", + DefaultTarget: big.NewInt(5), + NetworkTargetOverrides: overrides, + }, + } + + r := NewTargetMinBalancer(lggr, pluginConfigOverrides) unexecuted := make([]UnexecutedTransfer, 0, len(tc.pendingTransfers)) for _, tr := range tc.pendingTransfers { @@ -336,21 +345,30 @@ func TestTargetMinBalancer_ComputeTransfersToBalance_arb_eth_opt_pending_status_ for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + overrides := make(map[models.NetworkSelector]*big.Int) + g := graph.NewGraph() for net, b := range tc.balances { g.(graph.GraphTest).AddNetwork(net, graph.Data{ Liquidity: big.NewInt(b), NetworkSelector: net, MinimumLiquidity: big.NewInt(tc.minimums[net]), - TargetLiquidity: big.NewInt(tc.targets[net]), }) + overrides[net] = big.NewInt(tc.targets[net]) } assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, arb)) assert.NoError(t, g.(graph.GraphTest).AddConnection(arb, eth)) assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, opt)) assert.NoError(t, g.(graph.GraphTest).AddConnection(opt, eth)) - r := NewTargetMinBalancer(lggr) + pluginConfigOverrides := models.PluginConfig{ + RebalancerConfig: models.RebalancerConfig{ + Type: "target-and-min", + DefaultTarget: big.NewInt(5), + NetworkTargetOverrides: overrides, + }, + } + r := NewTargetMinBalancer(lggr, pluginConfigOverrides) unexecuted := make([]UnexecutedTransfer, 0, len(tc.pendingTransfers)) for _, tr := range tc.pendingTransfers { @@ -476,14 +494,15 @@ func TestTargetMinBalancer_ComputeTransfersToBalance_arb_eth_opt_base(t *testing for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + overrides := make(map[models.NetworkSelector]*big.Int) g := graph.NewGraph() for net, b := range tc.balances { g.(graph.GraphTest).AddNetwork(net, graph.Data{ Liquidity: big.NewInt(b), NetworkSelector: net, MinimumLiquidity: big.NewInt(tc.minimums[net]), - TargetLiquidity: big.NewInt(tc.targets[net]), }) + overrides[net] = big.NewInt(tc.targets[net]) } assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, arb)) assert.NoError(t, g.(graph.GraphTest).AddConnection(arb, eth)) @@ -492,7 +511,14 @@ func TestTargetMinBalancer_ComputeTransfersToBalance_arb_eth_opt_base(t *testing assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, base)) assert.NoError(t, g.(graph.GraphTest).AddConnection(base, eth)) - r := NewTargetMinBalancer(lggr) + pluginConfigOverrides := models.PluginConfig{ + RebalancerConfig: models.RebalancerConfig{ + Type: "target-and-min", + DefaultTarget: big.NewInt(5), + NetworkTargetOverrides: overrides, + }, + } + r := NewTargetMinBalancer(lggr, pluginConfigOverrides) unexecuted := make([]UnexecutedTransfer, 0, len(tc.pendingTransfers)) for _, tr := range tc.pendingTransfers { @@ -586,14 +612,16 @@ func TestTargetMinBalancer_ComputeTransfersToBalance_islands_in_graph(t *testing for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + overrides := make(map[models.NetworkSelector]*big.Int) + g := graph.NewGraph() for net, b := range tc.balances { g.(graph.GraphTest).AddNetwork(net, graph.Data{ Liquidity: big.NewInt(b), NetworkSelector: net, MinimumLiquidity: big.NewInt(tc.minimums[net]), - TargetLiquidity: big.NewInt(tc.targets[net]), }) + overrides[net] = big.NewInt(tc.targets[net]) } assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, arb)) assert.NoError(t, g.(graph.GraphTest).AddConnection(arb, eth)) @@ -602,7 +630,92 @@ func TestTargetMinBalancer_ComputeTransfersToBalance_islands_in_graph(t *testing assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, base)) assert.NoError(t, g.(graph.GraphTest).AddConnection(base, eth)) - r := NewTargetMinBalancer(lggr) + pluginConfigOverrides := models.PluginConfig{ + RebalancerConfig: models.RebalancerConfig{ + Type: "target-and-min", + DefaultTarget: big.NewInt(5), + NetworkTargetOverrides: overrides, + }, + } + r := NewTargetMinBalancer(lggr, pluginConfigOverrides) + + unexecuted := make([]UnexecutedTransfer, 0, len(tc.pendingTransfers)) + for _, tr := range tc.pendingTransfers { + unexecuted = append(unexecuted, models.PendingTransfer{ + Transfer: models.Transfer{ + From: tr.From, + To: tr.To, + Amount: tr.Amount, + }, + Status: tr.Status, + }) + } + transfersToBalance, err := r.ComputeTransfersToBalance(g, unexecuted) + assert.NoError(t, err) + + for _, tr := range transfersToBalance { + t.Logf("actual transfer: %s -> %s = %s", tr.From, tr.To, tr.Amount) + } + sort.Sort(models.ProposedTransfers(tc.expTransfers)) + require.Len(t, transfersToBalance, len(tc.expTransfers)) + for i, tr := range tc.expTransfers { + t.Logf("expected transfer: %s -> %s = %s", tr.From, tr.To, tr.Amount) + assert.Equal(t, tr.From, transfersToBalance[i].From) + assert.Equal(t, tr.To, transfersToBalance[i].To) + assert.Equal(t, tr.Amount.Int64(), transfersToBalance[i].Amount.Int64()) + } + }) + } +} + +func TestTargetMinBalancer_ComputeTransfersToBalance_no_tiny_transfers(t *testing.T) { + testCases := []struct { + name string + balances map[models.NetworkSelector]int64 + minimums map[models.NetworkSelector]int64 + targets map[models.NetworkSelector]int64 + pendingTransfers []models.ProposedTransfer + expTransfers []models.ProposedTransfer + }{ + { + name: "arb and opt below but not more than 5%, so even tho eth has funds we wait", + balances: map[models.NetworkSelector]int64{eth: 2100, arb: 1950, opt: 1950}, + minimums: map[models.NetworkSelector]int64{eth: 500, arb: 500, opt: 500}, + targets: map[models.NetworkSelector]int64{eth: 2000, arb: 2000, opt: 2000}, + pendingTransfers: []models.ProposedTransfer{}, + expTransfers: []models.ProposedTransfer{}, + }, + } + + lggr := logger.TestLogger(t) + lggr.SetLogLevel(zapcore.DebugLevel) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + overrides := make(map[models.NetworkSelector]*big.Int) + + g := graph.NewGraph() + for net, b := range tc.balances { + g.(graph.GraphTest).AddNetwork(net, graph.Data{ + Liquidity: big.NewInt(b), + NetworkSelector: net, + MinimumLiquidity: big.NewInt(tc.minimums[net]), + }) + overrides[net] = big.NewInt(tc.targets[net]) + } + assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, arb)) + assert.NoError(t, g.(graph.GraphTest).AddConnection(arb, eth)) + assert.NoError(t, g.(graph.GraphTest).AddConnection(eth, opt)) + assert.NoError(t, g.(graph.GraphTest).AddConnection(opt, eth)) + + pluginConfigOverrides := models.PluginConfig{ + RebalancerConfig: models.RebalancerConfig{ + Type: "target-and-min", + DefaultTarget: big.NewInt(5), + NetworkTargetOverrides: overrides, + }, + } + r := NewTargetMinBalancer(lggr, pluginConfigOverrides) unexecuted := make([]UnexecutedTransfer, 0, len(tc.pendingTransfers)) for _, tr := range tc.pendingTransfers {