Skip to content

Commit

Permalink
LM-24 Integrate minimum liquidities rebalancing algorithm (#1037)
Browse files Browse the repository at this point in the history
## Motivation


## Solution

---------

Co-authored-by: amirylm <[email protected]>
  • Loading branch information
cmalec and amirylm authored Jun 25, 2024
1 parent ffc4a2f commit 157c4df
Show file tree
Hide file tree
Showing 13 changed files with 1,251 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,11 @@ func (e *evmDiscoverer) getVertexData(ctx context.Context, v graph.Vertex) (grap

minimumLiquidity, err := lm.GetMinimumLiquidity(&bind.CallOpts{Context: ctx})
if err != nil {
return graph.Data{}, nil, fmt.Errorf("get target balance: %w", err)
return graph.Data{}, nil, fmt.Errorf("get minimum liquidity balance: %w", err)
}

//Do we want to add TargetLiquidity to the contract?

data := graph.Data{
Liquidity: liquidity,
TokenAddress: models.Address(token),
Expand Down
2 changes: 2 additions & 0 deletions core/services/ocr2/plugins/liquiditymanager/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func (p PluginFactory) buildRebalancer() (liquidityrebalancer.Rebalancer, error)
return liquidityrebalancer.NewPingPong(), nil
case models.RebalancerTypeMinLiquidity:
return liquidityrebalancer.NewMinLiquidityRebalancer(p.lggr), nil
case models.RebalancerTypeTargetAndMin:
return liquidityrebalancer.NewTargetMinBalancer(p.lggr), nil
default:
return nil, fmt.Errorf("invalid rebalancer type %s", p.config.RebalancerConfig.Type)
}
Expand Down
6 changes: 6 additions & 0 deletions core/services/ocr2/plugins/liquiditymanager/graph/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Data struct {
ConfigDigest models.ConfigDigest
NetworkSelector models.NetworkSelector
MinimumLiquidity *big.Int
TargetLiquidity *big.Int
}

func (d Data) Equals(other Data) bool {
Expand Down Expand Up @@ -60,6 +61,10 @@ func (d Data) Clone() Data {
if minLiq == nil {
minLiq = big.NewInt(0)
}
targetLiq := d.TargetLiquidity
if targetLiq == nil {
targetLiq = big.NewInt(0)
}
return Data{
Liquidity: big.NewInt(0).Set(liq),
TokenAddress: tokenAddr,
Expand All @@ -68,5 +73,6 @@ func (d Data) Clone() Data {
ConfigDigest: d.ConfigDigest.Clone(),
NetworkSelector: d.NetworkSelector,
MinimumLiquidity: big.NewInt(0).Set(minLiq),
TargetLiquidity: big.NewInt(0).Set(targetLiq),
}
}
27 changes: 27 additions & 0 deletions core/services/ocr2/plugins/liquiditymanager/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ type GraphReader interface {
IsEmpty() bool
// Len returns the number of vertices in the graph.
Len() int
// FindPath returns the path from the source to the destination network.
// The iterator function is called for each node in the path with the data of the node.
FindPath(from, to models.NetworkSelector, maxEdgesTraversed int, iterator func(nodes ...Data) bool) []models.NetworkSelector
}

// Graph contains graphs functionality for networks and liquidity
Expand All @@ -57,6 +60,8 @@ type Graph interface {
String() string
// Reset resets the graph to it's empty state.
Reset()
// Clone creates a deep copy of the graph.
Clone() Graph
}

// GraphTest provides testing functionality for the graph.
Expand Down Expand Up @@ -159,3 +164,25 @@ func (g *liquidityGraph) Reset() {
g.adj = make(map[models.NetworkSelector][]models.NetworkSelector)
g.data = make(map[models.NetworkSelector]Data)
}

func (g *liquidityGraph) Clone() Graph {
g.lock.RLock()
defer g.lock.RUnlock()

clone := &liquidityGraph{
adj: make(map[models.NetworkSelector][]models.NetworkSelector, len(g.adj)),
data: make(map[models.NetworkSelector]Data, len(g.data)),
}

for k, v := range g.adj {
adjCopy := make([]models.NetworkSelector, len(v))
copy(adjCopy, v)
clone.adj[k] = adjCopy
}

for k, v := range g.data {
clone.data[k] = v.Clone()
}

return clone
}
41 changes: 41 additions & 0 deletions core/services/ocr2/plugins/liquiditymanager/graph/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,47 @@ func (g *liquidityGraph) Len() int {
return g.len()
}

// FindPath finds a path from the source network to the destination network with the given number of edges that are allow to be traversed.
// It calls the iterator function with each individual node in the path.
// It returns the list of network selectors representing the path from source to destination (including the destination node).
func (g *liquidityGraph) FindPath(from, to models.NetworkSelector, maxEdgesTraversed int, iterator func(nodes ...Data) bool) []models.NetworkSelector {
g.lock.RLock()
defer g.lock.RUnlock()

return g.findPath(from, to, maxEdgesTraversed, iterator)
}

func (g *liquidityGraph) findPath(from, to models.NetworkSelector, maxEdgesTraversed int, iterator func(nodes ...Data) bool) []models.NetworkSelector {
if maxEdgesTraversed == 0 {
return []models.NetworkSelector{}
}
neibs, exist := g.adj[from]
if !exist {
return []models.NetworkSelector{}
}
for _, n := range neibs {
if n == to {
if !iterator(g.data[to]) {
continue
}
return []models.NetworkSelector{n}
}
}
for _, n := range neibs {
if p := g.findPath(n, to, maxEdgesTraversed-1, iterator); len(p) > 0 {
data := []Data{g.data[n]}
for _, d := range p {
data = append(data, g.data[d])
}
if !iterator(data...) {
continue
}
return append([]models.NetworkSelector{n}, p...)
}
}
return []models.NetworkSelector{}
}

func (g *liquidityGraph) getData(n models.NetworkSelector) (Data, bool) {
data, exists := g.data[n]
return data, exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (g *liquidityGraph) SetLiquidity(n models.NetworkSelector, liquidity *big.I
ConfigDigest: prev.ConfigDigest,
NetworkSelector: prev.NetworkSelector,
MinimumLiquidity: prev.MinimumLiquidity,
TargetLiquidity: prev.TargetLiquidity,
}
return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package liquidityrebalancer

import (
"fmt"
"math/big"

big2 "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/graph"
"github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models"
)

type Funds struct {
AvailableAmount *big.Int
}

// getExpectedGraph returns a copy of the graph instance with all the non-executed transfers applied.
func 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()))
}
}

return expG, nil
}

// mergeProposedTransfers merges multiple transfers with the same sender and recipient into a single transfer.
func mergeProposedTransfers(transfers []models.ProposedTransfer) []models.ProposedTransfer {
sums := make(map[[2]models.NetworkSelector]*big.Int)
for _, tr := range transfers {
k := [2]models.NetworkSelector{tr.From, tr.To}
if _, exists := sums[k]; !exists {
sums[k] = tr.TransferAmount()
continue
}
sums[k] = big.NewInt(0).Add(sums[k], tr.TransferAmount())
}

merged := make([]models.ProposedTransfer, 0, len(transfers))
for k, v := range sums {
merged = append(merged, models.ProposedTransfer{From: k[0], To: k[1], Amount: big2.New(v)})
}
return merged
}

func minBigInt(a, b *big.Int) *big.Int {
switch a.Cmp(b) {
case -1: // a < b
return a
case 0: // a == b
return a
case 1: // a > b
return b
}
return nil
}

// availableTransferableAmount calculates the available transferable amount of liquidity for a given network
// at two different time points (graphNow and graphLater).
// It takes a graph.Graph instance for the current time point (graphNow), a graph.Graph instance for the future time point (graphLater),
// and a models.NetworkSelector that represents the network for which to calculate the transferable amount.
// It returns the minimum of the available transferable amounts calculated from graphNow and graphLater as a *big.Int
func availableTransferableAmount(graphNow, graphLater graph.Graph, net models.NetworkSelector) (*big.Int, error) {
nowData, err := graphNow.GetData(net)
if err != nil {
return nil, fmt.Errorf("error during GetData for %d in graphNow: %v", net, err)
}
availableAmountNow := big.NewInt(0).Sub(nowData.Liquidity, nowData.MinimumLiquidity)
laterData, err := graphLater.GetData(net)
if err != nil {
return nil, fmt.Errorf("error during GetData for %d in graphLater: %v", net, err)
}
availableAmountLater := big.NewInt(0).Sub(laterData.Liquidity, laterData.MinimumLiquidity)
return minBigInt(availableAmountNow, availableAmountLater), nil
}

// getTargetLiquidityDifferences calculates the liquidity differences between two graph instances.
// It returns two maps, liqDiffsNow and liqDiffsLater, where each map contains the liquidity differences for each network.
// The function iterates over the networks in graphNow and graphLater and compares their target liquidity and liquidity values.
// If the target liquidity is set to 0, automated rebalancing is disabled for that network.
// The liquidity differences are calculated by subtracting the liquidity from the target liquidity.
// The function uses the models.NetworkSelector type to identify networks.
func getTargetLiquidityDifferences(graphNow, graphLater graph.Graph) (liqDiffsNow, liqDiffsLater map[models.NetworkSelector]*big.Int, err error) {
liqDiffsNow = make(map[models.NetworkSelector]*big.Int)
liqDiffsLater = make(map[models.NetworkSelector]*big.Int)

for _, net := range graphNow.GetNetworks() {
dataNow, err := graphNow.GetData(net)
if err != nil {
return nil, nil, fmt.Errorf("get data now of net %v: %w", net, err)
}

dataLater, err := graphLater.GetData(net)
if err != nil {
return nil, nil, fmt.Errorf("get data later of net %v: %w", net, err)
}

if dataNow.TargetLiquidity == nil {
return nil, nil, fmt.Errorf("target liquidity is nil for network %v", net)
}
if dataNow.TargetLiquidity.Cmp(big.NewInt(0)) == 0 {
// automated rebalancing is disabled if target is set to 0
liqDiffsNow[net] = big.NewInt(0)
liqDiffsLater[net] = big.NewInt(0)
continue
}

liqDiffsNow[net] = big.NewInt(0).Sub(dataNow.TargetLiquidity, dataNow.Liquidity)
liqDiffsLater[net] = big.NewInt(0).Sub(dataLater.TargetLiquidity, dataLater.Liquidity)
}

return liqDiffsNow, liqDiffsLater, nil
}

// filterUnexecutedTransfers filters out transfers that have already been executed.
func filterUnexecutedTransfers(nonExecutedTransfers []UnexecutedTransfer) []UnexecutedTransfer {
filtered := make([]UnexecutedTransfer, 0, len(nonExecutedTransfers))
for _, tr := range nonExecutedTransfers {
if tr.TransferStatus() != models.TransferStatusExecuted {
filtered = append(filtered, tr)
}
}
return filtered
}
Loading

0 comments on commit 157c4df

Please sign in to comment.