Skip to content

Automated Rebalancer#2835

Merged
shahthepro merged 59 commits intomasterfrom
shah/auto-rebalancer
Apr 13, 2026
Merged

Automated Rebalancer#2835
shahthepro merged 59 commits intomasterfrom
shah/auto-rebalancer

Conversation

@shahthepro
Copy link
Copy Markdown
Collaborator

@shahthepro shahthepro commented Mar 10, 2026

Code Changes

  • Add a new Safe Module RebalancerModule with methods to process deposits, withdrawals or both (in withdrawals -> deposits order). It will be replacing the existing AutoWithdrawalModule
  • Add a script for Rebalancer's core logic, Check readme file for the core logic
  • Add a hardhat tasks planRebalance that prints the optimal allocations and recommended actions
  • Add a Defender script that uses the Rebalancer to find recommended actions and execute it using the Module
  • Adds unit tests for the Module and the Rebalancer logic

Executing Hardhat task

$ npx hardhat planRebalance --network mainnet
> npx hardhat planRebalance --network mainnet

=== OUSD Rebalancer Status ===

Total rebalancable capital : 4,375,696.44 USDC
Withdrawal shortfall       : 0.00 USDC

--- Allocations ---

Strategy                        Current        Avail.         Target (rec.)   Delta  1h APY  Spot APY  Impact
-------------------------------------------------------------------------------------------------------------
Ethereum Morpho *  3,775,576.16 (86.3%)  1,685,960.71  3,775,576.16 (86.3%)   +0.00   4.47%     4.19%       —
Base Morpho          587,193.71 (13.4%)    336,733.85    587,193.71 (13.4%)   +0.00   4.35%     4.35%       —
HyperEVM Morpho        10,022.27 (0.2%)     10,022.52      10,022.27 (0.2%)   +0.00   4.82%     4.84%       —
Vault (idle)            2,904.30 (0.1%)             —       3,000.00 (0.1%)  +95.70       —         —       —
-------------------------------------------------------------------------------------------------------------
Total                      4,375,696.44
  * = default strategy

--- Actions for max APY ---

  DEPOSIT  $      487,098.01  to    Ethereum Morpho [Not recommended: post-impact spread -0.57% < min 0.50%]
  WITHDRAW $      587,193.71  from  Base Morpho [Not recommended: no approved deposits to fund]
  DEPOSIT  $      100,000.00  to    HyperEVM Morpho [Not recommended: post-impact spread 0.11% < min 0.50%]

--- Recommended Actions ---

  No actions required.

Pending Things

  • Monitoring: Discord Notifications Done in d50c610

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.66%. Comparing base (10b029e) to head (e10b3ea).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2835      +/-   ##
==========================================
+ Coverage   49.21%   51.66%   +2.45%     
==========================================
  Files         112      113       +1     
  Lines        4844     4916      +72     
  Branches     1343     1362      +19     
==========================================
+ Hits         2384     2540     +156     
+ Misses       2456     2372      -84     
  Partials        4        4              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@shahthepro shahthepro changed the title [WIP] Automated Rebalancer Automated Rebalancer Mar 12, 2026
Copy link
Copy Markdown
Member

@sparrowDom sparrowDom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done yet left comments inline

Comment thread contracts/scripts/defender-actions/ousdRebalancer.js Outdated
Comment thread contracts/scripts/defender-actions/ousdRebalancer.js
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/scripts/defender-actions/ousdRebalancer.js Outdated
Copy link
Copy Markdown
Collaborator

@naddison36 naddison36 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

@sparrowDom sparrowDom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some more comments added

Comment thread contracts/scripts/defender-actions/ousdRebalancer.js Outdated
Comment thread contracts/utils/rebalancer.js Outdated
Comment thread contracts/scripts/defender-actions/ousdRebalancer.js Outdated
Copy link
Copy Markdown
Member

@sparrowDom sparrowDom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe cappedAmount would be a better name

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: b8c3299

Comment thread contracts/scripts/defender-actions/ousdRebalancer.js Outdated
// 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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, the safe module already behaves that way when you call that processWithdrawalsAndDeposits method. Will drop the other two methods

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It simplified the code even further, thank you: 4a0a253

shortfall,
constraints
) {
const totalRebalancable = strategies.reduce(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should minDefaultStrategyBps be excluded from totalRebalancable?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is much better readable from the last time. Great improvement 👍

Comment thread contracts/utils/rebalancer.js Outdated

const sorted = [...strategies]
.filter((s) => s.address !== defaultStrategy.address)
.sort((a, b) => (targets[b.address].gt(targets[a.address]) ? 1 : -1));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

@shahthepro shahthepro Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread contracts/utils/rebalancer.js Outdated
.filter((a) => a.action === ACTION_DEPOSIT)
.sort((a, b) => b.apy - a.apy);

for (const c of deposits) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: instead of c this could be named deposit

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread contracts/utils/rebalancer.js Outdated
}

const amt = c.delta.gt(budget) ? budget : c.delta;
budget = budget.sub(amt);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: 12e5859

const hasApprovedWithdrawals = result.some(
(a) => a.action === ACTION_WITHDRAW
);
if (!hasApprovedWithdrawals && shortfall.gt(0)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could it be that there are approved withdrawals that don't withdraw enough to cover the shortfall?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

* Consider APY impact for deposits

* Fix config issues

* Bug fixes

* Bug fix

* bug fix

* few more tweaks

* Switch to using subsquid server

* APY Impact solver

* Fix available liquidity

* Address CR comments
clement-ux
clement-ux previously approved these changes Apr 9, 2026
@shahthepro shahthepro requested a review from naddison36 April 9, 2026 17:28
@shahthepro shahthepro merged commit 70c2c1c into master Apr 13, 2026
17 of 18 checks passed
@shahthepro shahthepro deleted the shah/auto-rebalancer branch April 13, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants