Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2835 +/- ##
==========================================
- Coverage 48.07% 42.23% -5.84%
==========================================
Files 111 112 +1
Lines 4826 4880 +54
Branches 1334 1348 +14
==========================================
- Hits 2320 2061 -259
- Misses 2502 2816 +314
+ Partials 4 3 -1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
sparrowDom
left a comment
There was a problem hiding this comment.
Not done yet left comments inline
naddison36
left a comment
There was a problem hiding this comment.
I think we need to whitelist what strategies the RebalancerModule can automatically deposit/withdraw to/from. I'm mostly worried about AMO strategies being accidentally being called without a vault value checker.
sparrowDom
left a comment
There was a problem hiding this comment.
Left some comments inline: This code is much easier to follow now. Thanks it is a great improvement
| ]; | ||
|
|
||
| // Return the action amount, capping cross-chain moves at the bridge limit | ||
| const actionAmount = (a) => { |
There was a problem hiding this comment.
nit: maybe cappedAmount would be a better name
| // All withdrawals are same-chain: freed USDC lands in the vault immediately, | ||
| // so withdrawals and deposits can be batched into a single transaction. | ||
| await executeTx(() => | ||
| rebalancerModule.processWithdrawalsAndDeposits( |
There was a problem hiding this comment.
nit: the on-chain contract could have just the processWithdrawalsAndDeposits function exposed. And have empty arrays passed when there would be no deposits or no withdrawals.
There was a problem hiding this comment.
That's a good point, the safe module already behaves that way when you call that processWithdrawalsAndDeposits method. Will drop the other two methods
There was a problem hiding this comment.
It simplified the code even further, thank you: 4a0a253
| shortfall, | ||
| constraints | ||
| ) { | ||
| const totalRebalancable = strategies.reduce( |
There was a problem hiding this comment.
Should minDefaultStrategyBps be excluded from totalRebalancable?
There was a problem hiding this comment.
I believe that'll complicate the script further. If we are subtracting that here, we should also subtract it from the defaultStrategy's balance when we query it. Otherewise, if the default strategy has less than the amount we subtract here, it might end with us having a few more conditional statements (like subtracting it from other strategies). I'm not sure if that's gonna work
| /** | ||
| * Compute total capital minus reserved amounts (shortfall + minVaultBalance). | ||
| */ | ||
| function _computeDeployableCapital( |
There was a problem hiding this comment.
This code is much better readable from the last time. Great improvement 👍
|
|
||
| const sorted = [...strategies] | ||
| .filter((s) => s.address !== defaultStrategy.address) | ||
| .sort((a, b) => (targets[b.address].gt(targets[a.address]) ? 1 : -1)); |
There was a problem hiding this comment.
So the sort mechanics here is to sort by the amount to be deposited. Would it make sense to sort by the APY strategy is earning instead?
Otherwise we might always deduct from the strategy having the largest amount deposited which might also be the one earning the highest APY.
There was a problem hiding this comment.
I thought of doing it by APY at first. But then if we are allocating it by APY, the lower APY strategies will have less liquidity. So, it means that they may not have enough liquidity to fund from a single strategy. So, it'll involve multiple withdraws from lower APY strategy, which would only make it gas expensive
contracts/utils/rebalancer.js
Outdated
| .filter((a) => a.action === ACTION_DEPOSIT) | ||
| .sort((a, b) => b.apy - a.apy); | ||
|
|
||
| for (const c of deposits) { |
There was a problem hiding this comment.
nit: instead of c this could be named deposit
contracts/utils/rebalancer.js
Outdated
| } | ||
|
|
||
| const amt = c.delta.gt(budget) ? budget : c.delta; | ||
| budget = budget.sub(amt); |
There was a problem hiding this comment.
Shouldn't this budget subtraction happen after the additional if statement checks below that result in a continue? Budget is reduced even when the action can be invalidated by setting it to ACTION_NONE
| const hasApprovedWithdrawals = result.some( | ||
| (a) => a.action === ACTION_WITHDRAW | ||
| ); | ||
| if (!hasApprovedWithdrawals && shortfall.gt(0)) { |
There was a problem hiding this comment.
could it be that there are approved withdrawals that don't withdraw enough to cover the shortfall?
There was a problem hiding this comment.
Right now, the rebalancer cannot directly move between strategies. So, if it has to move between strategies, it has to withdraw to Vault first and then to the other strategy (either in a single run or across multiple runs). So, if there's a withdraw action, it'll always cover the withdrawal shortfall
naddison36
left a comment
There was a problem hiding this comment.
I think the biggest issue with the current approach is it doesn't take into account the impact of reallocating funds. The danger is it will reallocate based on the current net APY, after the reallocation the APYs change and the funds are reallocated back. We don't want to end up in a situation where funds are reallocated back and forth.
| const ACTION_NONE = "none"; | ||
|
|
||
| // Human-readable ABIs for contracts we interact with | ||
| const vaultAbi = [ |
There was a problem hiding this comment.
nit: this can be imported from an existing ABI file
const vaultAbi = require("../abi/vault.json");| "function withdrawalQueueMetadata() external view returns (tuple(uint128 queued, uint128 claimable, uint128 claimed, uint128 nextWithdrawalIndex))", | ||
| ]; | ||
|
|
||
| const strategyAbi = [ |
There was a problem hiding this comment.
nit: I think it would be cleaner if the cross chain strategy ABI was added to the ../abi/ folder. Then the following could be used
const strategyAbi = require("../abi/crossChainStrategy.json");| "function isTransferPending() external view returns (bool)", | ||
| ]; | ||
|
|
||
| const erc20Abi = [ |
There was a problem hiding this comment.
nit: this can be replaced with
const erc20Abi = require("../abi/erc20.json");| ]); | ||
|
|
||
| // Reserve any available vault balance for pending withdrawals | ||
| let shortfall = queueMeta.queued.sub(queueMeta.claimable); |
There was a problem hiding this comment.
this should be moved into its own util but no need to do as part of this PR.
I'll do it in a separate JS refactoring PR.
Leave it as is for now
| state.strategies.filter((s) => s.morphoVaultAddress) | ||
| ); | ||
|
|
||
| // Exclude strategies with suspiciously high APY |
There was a problem hiding this comment.
a nice feature. It'd be interesting to see historical APYs to know how high they can get. A 50% APY seems very high, especially if its an average over 6 hours.
I expect a spike to 50% for some number of blocks would be possible.
Is the 50% used from gut feel?
There was a problem hiding this comment.
Yea. The config for everything is just rough values, not final ones. We probably have to sync with everyone to decide on those constraints before we deploy
| } | ||
|
|
||
| /** | ||
| * Fetch APYs for multiple vaults in parallel. |
There was a problem hiding this comment.
This is a critical function. Let's make it very clear what APY we are getting
* Fetch a single vault's current net APY after fees from the Morpho GraphQL API.
* The APY is a weighted average based on the liquidity allocated in each market.
contracts/utils/rebalancer.md
Outdated
| | Field | Value | Meaning | | ||
| |-------|-------|---------| | ||
| | `minDefaultStrategyBps` | 2000 | Default strategy always gets ≥ 20 % of deployable capital | | ||
| | `maxPerStrategyBps` | 7000 | No single strategy gets > 70 % | |
There was a problem hiding this comment.
70% seems too low given we only have two strategies at the moment.
If Rebalancer was running now, 700k would be moved from Ethereum earning 4.23% to Base earning 3.64%.
Even if we had 5 Morpho strategies, if one was earning 5% and all the others were only earning 1%, would we really want 30% allocated to the vaults earning 1%?
I'd say the maxPerStrategyBps constraint is dropped and minVaultBalance is used to ensure not everything is allocated to a single vault.
Alternatively, increase maxPerStrategyBps to 90% or even 95%.
There was a problem hiding this comment.
Yep. Like I mentioned in the other comment, the values in the config file aren't finalised. Can change it
* Updated comments on what the Morpho APY is * Made it clear the strategies are only for Morpho * Fixed alignment of Allocations data in planBalance output
Code Changes
RebalancerModulewith methods to process deposits, withdrawals or both (in withdrawals -> deposits order). It will be replacing the existingAutoWithdrawalModuleplanRebalancethat prints the optimal allocations and recommended actionsExecuting Hardhat task
Pending Things
Monitoring: Discord NotificationsDone in d50c610