-
Notifications
You must be signed in to change notification settings - Fork 102
feat: Add RebalancerClient module enabling inventory rebalancing via token swapping (e.g. USDC any chain to USDT any chain)
#2826
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
Open
nicholaspai
wants to merge
90
commits into
master
Choose a base branch
from
rebalancer-scaffolding
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
… 2.0 This PR is designed to open the discussion on designing an improved inventory manager system that supports the following features: - Allows arbitrary any to any rebalances - Allows user to specify target balances instead of percentages, which are easier to reason about The current system is limited to L1->L2 and L2->L1 flows and it handles them separately with different clients, this makes the code harder to maintain and also difficult to implement L2 to L2 rebalancing.
next up is to place an order in initializeRebalances
…apter and also add opportunity cost component for OFT source bridges
RebalancerClient module enabling inventory rebalancing via token swapping (e.g. USDC any chain to USDT any chain)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Nomenclature
In this description, when I refer to
RebalancerI am referring to the new module that I am introducing in this PR. Contrast theRebalancerwith theInventoryClientwhich currently bridges tokens between chains to "rebalance" inventory.Eventually, the plan is for the rebalancing logic in the
InventoryClientto be taken over by the logic in theRebalancer.To start, the
Rebalancerwill only be responsible for rebalancing inventory across chains and swapping that inventory between currencies (i.e. USDC on a chain to USDT on another chain; could be the same chain). TheInventoryClientwill remain responsible for bridging tokens across chains without swapping those tokens between currencies.Overview
This PR introduces a
RebalancerClientthat can be used to swap inventory between USDC and USDT across any chains. The list of eligible chains are those that either have (i) CCTP support, (ii) OFT support, or (iii) support for depositing into Binance.The
RebalancerClientallows the user to pass in a distribution of token balances across all chains alongside a "target" distribution. For any chains that are under target, the client will attempt to draw balance from those chains that are over target and swap these excess distributions to fulfill deficits.For example, if the current distribution of balances appears to be underweight USDC balance on Arbitrum and overweight USDT balance on Optimism, then the rebalancer will attempt to swap that USDC into USDT and send USDT to Optimism.
The rebalancer will find the cheapest path to connect these excesses to deficits amongst two options provided in this initial PR: swapping on Hyperliquid and swapping on Binance.
The user of the
RebalancerClientcan track all outstanding rebalances and get a list of virtual balances across all chains representing in-flight rebalances. These virtual balances are designed to then be applied to the user's current assumption about its inventory distribution in order to produce a more accurate view of their distribution.Intended usage
The
RebalancerClientis currently designed to be run in the same manner that theInventoryClientdoes today. I envision theinventoryClientkeeping its view of current balances accurate by using the returned virtual balances fromRebalancerClient.getPendingRebalances(). Moreover, I expect that theinventoryClient.getTokenDistributionPerL1Token()function is used as input into theRebalancerClient.initializeRebalance()function.By using the
RebalancerClientin this PR alongside theInventoryClient, we can not only continue to rebalance inventory across chains by bridging equivalent tokens across chains, but we can also define per-chain target balances and use excess balances to fill any under target chain balances.RebalancerClientConstructing
Config
The client requires a configuration containing a
targetBalancesobject of type:{ [chainId: number]: { [tokenSymbol: string]: { targetBalance: BigNumber; priorityTier: number } }. This will be compared against thecurrentBalancesobject of an identical name when evaluating new rebalance opportunities.The
priorityTierfor eachtargetBalanceis a way to break ties between different chains that each are undertarget. In some cases, it's not appropriate to simply rank target deficits by simply comparing(targetBalance - currentBalance). For example, we might want to treat a chain likeEthereum Mainnetas a "universal sink" whosetargetBalance = MAX_INTEGERbecause in most cases, we want to send excess L2 inventory back to L1. However, setting thetargetBalanceto this high of a number means that its ""deficit" amount will always outrank any other chain's deficit if we simply compared(target - current). Therefore, I believe it will be helpful for us to have a second dimension for ranking deficits/excesses with thepriorityTierscalar.Adapters
We pass in a list of
adaptersinto the client that implement ways to swap between tokens represented in thetargetBalances. Eachadaptermust implement an interface:Rebalance Routes
We pass a list of rebalance routes into the client's constructor which are the client's available options to redistribute its
currentBalancestowards its desiredtargetBalancesdistribution. Two routes can have identicalsourceChain/TokenanddestinationChain/Tokenbut differ in theadapterproperty, and then the client should pick the "cheapest" option as per the user's constraints. ThemaxAmountToTransferproperty is probably not strictly required but I found it useful to make sure I didn't accidentally send too many funds into any exchange during my local testing. I essentially treatmaxAmountToTransferas a way to throttle the amount of funds I'm sending per test.Initializing
The initializer, unlike the constructor, can be an
asyncfunction so i've found it useful to do one-time set up things here like checking allowances and evaluating rebalance routes for each adapter.The
RebalancerClientsimply callsadapter.initialize()for eachadapterits constructed with.Public interface
There is only a single function I've implemented so far:
I ideally would like to see
currentBalancesis close to the value returned byinventoryClient.getTokenDistribution()plus any virtual balance credits and deficits returned by eachadapter.getPendingRebalances()function. See the following code snippet that I've copied fromsrc/rebalancer/index.ts:Implementation Details
Code Structure
Modules
Adapters
Each adapter provides logic to
The following adapters are built with the following features:
sourceChainordestinationChainis not a Binance supported network, like HyperEVM or Unichain for example.Arbitrumis hardcoded as the default network to deposit into Binance if thesourceChainordestinationChaindoesn't have a Binance connection.sourceChainordestinationChainis notHyperEVM, then tokens are bridged to/fromHyperEVMvia CCTP/OFT.getPendingRebalanceswhich is used to track any bridges arising from the Binance or HL adapter rebalances.Controller
The
RebalancerClient.rebalanceInventory(currentBalances, maxFeePct)method performs the following heuristic, which can be improved in the future:sourceChain+sourceTokensources have "excess" balance by comparingcurrentandtargetbalances. (Ignore chains wherecurrentBalance <= targetBalancethat don't have an excess.)destinationChain+destinationTokensinks have "deficit" balance using similar logic to (1). (Ignore chains wherecurrentBalance >= targetBalancethat don't have an deficit.)deficitsin descending order from highestpriorityTierto lowest, using(targetBalance - currentBalance)as the tie breaker. This essentially orders chains by the highest priority deficits we want to fill.excessesin ascending order from lowestpriorityTierto highest, using(currentBalance - targetBalance)as the tie breaker. The simple intuition ofpriorityTieris a scalar to represent the willingness that we want to keep balance on that chain, so lower priority tier chains are the first to remove excess balance from and the last to fill deficits for. In practice, I envision setting thepriorityTierforEthereum Mainnetto be 0 and set itstargetBalance = MAX_INTEGERso that we send excess balance from every chain back toEthereumonly after evaluating all other L2 target deficits.deficitsand find the firstexcessthat matches with it. To match, there must be at least onerebalanceRouteconfigured in theRebalancerClientwhereroute.sourceChain = excess.chain && route.sourceToken = excess.token && route.destinationChain = deficit.chain && route.destinationToken = deficit.tokenfor we are always rebalancing from excess to deficit.deficitandexcess, calculate the expected cost to rebalance from deficit to excess over that route by callingroute.adapter.getEstimatedCost(route)and choose the lowest cost less thanmaxFeePct. An snippet of this logic is:Integration with existing
InventoryClientI envision parts of the
RebalancerClientbeing upstream as well as downstream of theInventoryClient's logic as executed insrc/relayer/index.ts.It is upstream in that the
RebalancerClientshould ultimately expose a method to query each of its adapter'sgetPendingRebalances()functions, aggregate their results, and provide these virtual balance modifications to theInventoryClient. This way theInventoryClient'sgetCurrentBalance()function can be more accurate.Something like the following can be used by the
InventoryClient:The
RebalancerClientis also downstream in thatrebalanceInventory(currentBalances)takes in acurrentBalancesstruct that should be the result of the aforementionedInventoryClient.getTokenDistribution()method. This way newRebalanceClient-originated rebalances are issued using the latest balance calculations.Preventing desync problems
The
RebalancerClientkeeps track of all pending orders in theRediscache. Orders are updated only when an adapter'supdateRebalanceStatuses()method is called. Each adapter'sgetPendingRebalances()should always return the most up to date state of balances.If there is a use case in which we want to fetch historical pending rebalances (i.e. pending rebalances at a snapshot in time in the past), then we can implement this by passing in timestamp boundaries into
getPendingRebalancesbut I haven't implemented this yet as I don't really see a need for this currently... please correct me if I'm mistaken.Binance race conditions
The
binanceFinalizercurrently queries all deposits into Binance and all withdrawals out of Binance when determining how much more should be withdrawn from Binance. All deposits are currently factored into this accounting, which means that any deposits sent by theRebalancermight get withdrawn prematurely before they can be swapped by thebinanceFinalizer.This PR introduces helper methods in
src/utils/BinanceUtilsthat can be used to "tag" Binance deposits and withdrawals with a "SWAP" type that thebinanceFinalizercan subsequently filter for and remove from the deposits that it counts when calculating how much to withdraw from Binance.Testing
I'm currently testing using the
rebalancer/index.tsfile with the commandts-node ./index.ts --rebalancer2 --wallet mnemonic. I don't necessarily envision thisindex.tsfile being part of our production stack but its a good example implementation of how we could use theRebalancerClientin practice.