Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
64a8c30
Add core logic and safe module
shahthepro Mar 10, 2026
17c663b
Update core logic
shahthepro Mar 12, 2026
38ebdb9
Add defender action
shahthepro Mar 12, 2026
923944d
Rename methods, add readme file
shahthepro Mar 12, 2026
d50c610
Push discord notification and rollup config changes
shahthepro Mar 16, 2026
3754149
Fix AMO balance
shahthepro Mar 16, 2026
024277b
Merge branch 'master' into shah/auto-rebalancer
shahthepro Mar 18, 2026
d15d24e
Add whitelist to module and a few code changes
shahthepro Mar 18, 2026
1de24f3
tweaks to rebalancer
shahthepro Mar 18, 2026
c649822
Refactor and simplify rebalancer
shahthepro Mar 18, 2026
614c9d8
More refactor
shahthepro Mar 19, 2026
6cd5f76
Bug fixes
shahthepro Mar 19, 2026
54bc1d1
prettify
shahthepro Mar 19, 2026
1ddc3ab
Cleanup
shahthepro Mar 19, 2026
55d8f2d
prettify
shahthepro Mar 19, 2026
7c0bcde
Fix comment
shahthepro Mar 19, 2026
e0fe58b
Add suspicious APY threshold
shahthepro Mar 19, 2026
8c5073c
Merge branch 'master' into shah/auto-rebalancer
shahthepro Mar 19, 2026
cfe03d4
Fix string comparison
shahthepro Mar 20, 2026
b93ed30
Merge branch 'master' into shah/auto-rebalancer
shahthepro Mar 20, 2026
cb54213
Merge branch 'master' into shah/auto-rebalancer
shahthepro Mar 24, 2026
12e5859
simplify budget calc
shahthepro Mar 24, 2026
55a63e3
Rename local var
shahthepro Mar 24, 2026
b8c3299
Rename to cappedAmount
shahthepro Mar 24, 2026
4a0a253
simplify contract
shahthepro Mar 24, 2026
59dbebc
Nicka/auto rebalancer (#2855)
naddison36 Mar 25, 2026
e1d3a4d
Add hyperEVM to config
shahthepro Mar 25, 2026
f486511
HyperEVM API fixes
shahthepro Mar 26, 2026
33ddd41
Limit by available liquidity
shahthepro Mar 26, 2026
e5c1493
Rename optimal to ideal
shahthepro Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ pnpm hardhat setActionVars --id b1d831f1-29d4-4943-bb2e-8e625b76e82c
pnpm hardhat setActionVars --id 6567d7c6-7ec7-44bd-b95b-470dd1ff780b
pnpm hardhat setActionVars --id 6a633bb0-aff8-4b37-aaae-b4c6f244ed87
pnpm hardhat setActionVars --id 076c59e4-4150-42c7-9ba0-9962069ac353
pnpm hardhat setActionVars --id ca80b55c-f3f7-4e03-a2f5-ea444645f8d9
pnpm hardhat setActionVars --id aa194c13-0dbf-49d2-8e87-70e61f3d71a8
pnpm hardhat setActionVars --id 65b53496-e426-4850-8349-059e63eb2120

Expand All @@ -372,6 +373,7 @@ pnpm hardhat updateAction --id b1d831f1-29d4-4943-bb2e-8e625b76e82c --file claim
pnpm hardhat updateAction --id 6567d7c6-7ec7-44bd-b95b-470dd1ff780b --file manageBribeOnSonic
pnpm hardhat updateAction --id 6a633bb0-aff8-4b37-aaae-b4c6f244ed87 --file managePassThrough
pnpm hardhat updateAction --id 076c59e4-4150-42c7-9ba0-9962069ac353 --file manageBribes
pnpm hardhat updateAction --id ca80b55c-f3f7-4e03-a2f5-ea444645f8d9 --file ousdRebalancer
pnpm hardhat updateAction --id aa194c13-0dbf-49d2-8e87-70e61f3d71a8 --file manageMerklBribes # Mainnet
pnpm hardhat updateAction --id 65b53496-e426-4850-8349-059e63eb2120 --file manageMerklBribes # Base

Expand Down
257 changes: 257 additions & 0 deletions contracts/contracts/automation/RebalancerModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { AbstractSafeModule } from "./AbstractSafeModule.sol";

import { IVault } from "../interfaces/IVault.sol";
import { VaultStorage } from "../vault/VaultStorage.sol";

/**
* @title Rebalancer Module
* @notice A Gnosis Safe module that automates OUSD vault rebalancing by
* withdrawing from overallocated strategies and depositing to
* underallocated strategies.
*
* @dev The Safe (Guardian multisig) must:
* 1. Deploy this module
* 2. Call `safe.enableModule(address(this))` to authorize it
*
* An off-chain operator (e.g. Defender Action) calls
* `processWithdrawalsAndDeposits` periodically with computed strategy/amount
* arrays. Either array may be empty. All intelligence (APY fetching, target
* allocation, constraint enforcement) lives off-chain. This contract is a
* dumb executor.
*
* The function uses soft failures: if a single strategy call fails via the
* Safe, the module emits an event and continues to the next strategy rather
* than reverting the entire batch.
*
* The Safe retains full control via `setPaused`.
*/
contract RebalancerModule is AbstractSafeModule {
// ───────────────────────────────────────────────────────── Immutables ──

/// @notice The vault whose strategies are being rebalanced.
IVault public immutable vault;

/// @notice The vault's base asset (e.g. USDC for OUSD).
address public immutable asset;

// ────────────────────────────────────────────────────── Mutable config ──

/// @notice When true, processWithdrawalsAndDeposits is blocked.
bool public paused;

/// @notice Strategies that this module is permitted to withdraw from or deposit into.
mapping(address => bool) public isAllowedStrategy;

// ─────────────────────────────────────────────────────────── Events ──

/// @notice Emitted after processWithdrawals completes (even if some failed).
event WithdrawalsProcessed(
address[] strategies,
uint256[] amounts,
uint256 remainingShortfall
);

/// @notice Emitted after processDeposits completes (even if some failed).
event DepositsProcessed(address[] strategies, uint256[] amounts);

/// @notice Emitted when a single withdrawFromStrategy call fails via the Safe.
event WithdrawalFailed(address indexed strategy, uint256 attemptedAmount);

/// @notice Emitted when a single depositToStrategy call fails via the Safe.
event DepositFailed(address indexed strategy, uint256 attemptedAmount);

/// @notice Emitted when the paused state changes.
event PausedStateChanged(bool paused);

/// @notice Emitted when a strategy is added to the whitelist.
event StrategyAllowed(address indexed strategy);

/// @notice Emitted when a strategy is removed from the whitelist.
event StrategyRevoked(address indexed strategy);

// ─────────────────────────────────────────────────────── Constructor ──

/**
* @param _safeContract Address of the Gnosis Safe (Guardian multisig).
* @param _operator Address of the off-chain operator (e.g. Defender relayer).
* @param _vault Address of the OUSD vault.
*/
constructor(
address _safeContract,
address _operator,
address _vault
) AbstractSafeModule(_safeContract) {
require(_vault != address(0), "Invalid vault");

vault = IVault(_vault);
asset = IVault(_vault).asset();

_grantRole(OPERATOR_ROLE, _operator);
}

// ──────────────────────────────────────────────────────── Modifiers ──

modifier whenNotPaused() {
require(!paused, "Module is paused");
_;
}

// ──────────────────────────────────────────────── Core automation ──

/**
* @notice Withdraw from overallocated strategies then deposit to underallocated
* ones. Either array may be empty — the contract loops over zero entries
* without reverting.
*
* @param _withdrawStrategies Strategies to withdraw from.
* @param _withdrawAmounts Amounts to withdraw from each strategy.
* @param _depositStrategies Strategies to deposit into.
* @param _depositAmounts Amounts to deposit into each strategy.
*/
function processWithdrawalsAndDeposits(
address[] calldata _withdrawStrategies,
uint256[] calldata _withdrawAmounts,
address[] calldata _depositStrategies,
uint256[] calldata _depositAmounts
) external onlyOperator whenNotPaused {
require(
_withdrawStrategies.length == _withdrawAmounts.length,
"Withdraw array length mismatch"
);
require(
_depositStrategies.length == _depositAmounts.length,
"Deposit array length mismatch"
);
vault.addWithdrawalQueueLiquidity();
_executeWithdrawals(_withdrawStrategies, _withdrawAmounts);
_executeDeposits(_depositStrategies, _depositAmounts);
emit WithdrawalsProcessed(
_withdrawStrategies,
_withdrawAmounts,
pendingShortfall()
);
emit DepositsProcessed(_depositStrategies, _depositAmounts);
}

// ─────────────────────────────────────── Guardian controls ──

/**
* @notice Pause or unpause the module.
* @param _paused True to pause, false to unpause.
*/
function setPaused(bool _paused) external onlySafe {
paused = _paused;
emit PausedStateChanged(_paused);
}

/**
* @notice Add a strategy to the whitelist, allowing the operator to move
* funds into or out of it.
* @param _strategy Strategy address to allow.
*/
function allowStrategy(address _strategy) external onlySafe {
require(_strategy != address(0), "Invalid strategy");
isAllowedStrategy[_strategy] = true;
emit StrategyAllowed(_strategy);
}

/**
* @notice Remove a strategy from the whitelist.
* @param _strategy Strategy address to revoke.
*/
function revokeStrategy(address _strategy) external onlySafe {
isAllowedStrategy[_strategy] = false;
emit StrategyRevoked(_strategy);
}

// ──────────────────────────────────────────────────────── View helpers ──

/**
* @notice The current unmet shortfall in the vault's withdrawal queue.
* @dev This is a raw read of `queued - claimable`. It does NOT account for
* idle vault asset that `addWithdrawalQueueLiquidity()` would absorb.
* For a fully up-to-date figure, call `vault.addWithdrawalQueueLiquidity()`
* first (which is what `processWithdrawals` does).
* @return shortfall Queue shortfall in asset units (vault asset decimals).
*/
function pendingShortfall() public view returns (uint256 shortfall) {
VaultStorage.WithdrawalQueueMetadata memory meta = vault
.withdrawalQueueMetadata();
shortfall = meta.queued - meta.claimable;
}

// ──────────────────────────────────────────────── Internal helpers ──

/// @dev Execute withdrawFromStrategy for each (strategy, amount) pair via the Safe.
function _executeWithdrawals(
address[] calldata _strategies,
uint256[] calldata _amounts
) internal {
address[] memory assets = _toAddressArray(asset);
for (uint256 i = 0; i < _strategies.length; i++) {
if (_amounts[i] == 0) continue;
require(isAllowedStrategy[_strategies[i]], "Strategy not allowed");
bool success = safeContract.execTransactionFromModule(
address(vault),
0,
abi.encodeWithSelector(
IVault.withdrawFromStrategy.selector,
_strategies[i],
assets,
_toUint256Array(_amounts[i])
),
0
);
if (!success) {
emit WithdrawalFailed(_strategies[i], _amounts[i]);
}
}
}

/// @dev Execute depositToStrategy for each (strategy, amount) pair via the Safe.
function _executeDeposits(
address[] calldata _strategies,
uint256[] calldata _amounts
) internal {
address[] memory assets = _toAddressArray(asset);
for (uint256 i = 0; i < _strategies.length; i++) {
if (_amounts[i] == 0) continue;
require(isAllowedStrategy[_strategies[i]], "Strategy not allowed");
bool success = safeContract.execTransactionFromModule(
address(vault),
0,
abi.encodeWithSelector(
IVault.depositToStrategy.selector,
_strategies[i],
assets,
_toUint256Array(_amounts[i])
),
0
);
if (!success) {
emit DepositFailed(_strategies[i], _amounts[i]);
}
}
}

function _toAddressArray(address _addr)
internal
pure
returns (address[] memory arr)
{
arr = new address[](1);
arr[0] = _addr;
}

function _toUint256Array(uint256 _val)
internal
pure
returns (uint256[] memory arr)
{
arr = new uint256[](1);
arr[0] = _val;
}
}
18 changes: 18 additions & 0 deletions contracts/contracts/mocks/MockAutoWithdrawalVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ contract MockAutoWithdrawalVault {
VaultStorage.WithdrawalQueueMetadata public withdrawalQueueMetadata;

bool private _revertNextWithdraw;
bool private _revertNextDeposit;

event MockedWithdrawal(address strategy, address asset, uint256 amount);
event MockedDeposit(address strategy, address asset, uint256 amount);

constructor(address _asset) {
asset = _asset;
Expand All @@ -27,6 +29,10 @@ contract MockAutoWithdrawalVault {
_revertNextWithdraw = true;
}

function revertNextDeposit() external {
_revertNextDeposit = true;
}

function addWithdrawalQueueLiquidity() external {
// Do nothing
}
Expand All @@ -42,4 +48,16 @@ contract MockAutoWithdrawalVault {
}
emit MockedWithdrawal(strategy, assets[0], amounts[0]);
}

function depositToStrategy(
address strategy,
address[] memory assets,
uint256[] memory amounts
) external {
if (_revertNextDeposit) {
_revertNextDeposit = false;
revert("Mocked deposit revert");
}
emit MockedDeposit(strategy, assets[0], amounts[0]);
}
}
5 changes: 5 additions & 0 deletions contracts/deploy/deployActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,11 @@ const deploySafeModulesForUnitTests = async () => {
mockAutoWithdrawalVault.address,
addresses.dead,
]);
await deployWithConfirmation("RebalancerModule", [
cSafeContract.address,
cSafeContract.address,
mockAutoWithdrawalVault.address,
]);
};

module.exports = {
Expand Down
39 changes: 39 additions & 0 deletions contracts/deploy/mainnet/182_ousd_rebalancer_module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const addresses = require("../../utils/addresses");
const {
deploymentWithGovernanceProposal,
deployWithConfirmation,
} = require("../../utils/deploy");

module.exports = deploymentWithGovernanceProposal(
{
deployName: "182_ousd_rebalancer_module",
forceDeploy: false,
reduceQueueTime: true,
deployerIsProposer: false,
proposalId: "",
},
async () => {
const safeAddress = addresses.multichainStrategist;

const cVaultProxy = await ethers.getContract("VaultProxy");

await deployWithConfirmation("RebalancerModule", [
safeAddress,
// Defender relayer
addresses.mainnet.validatorRegistrator,
cVaultProxy.address,
]);
const cRebalancerModule = await ethers.getContract("RebalancerModule");
console.log(`RebalancerModule deployed to ${cRebalancerModule.address}`);

// TODO: After deployment, the Guardian Safe must call allowStrategy() for each
// strategy the rebalancer is permitted to touch.
// Submit a separate Safe transaction for:
// - addresses.mainnet.MorphoOUSDv2StrategyProxy (Ethereum Morpho)
// - addresses.mainnet.CrossChainMasterStrategy (Base Morpho master)

return {
actions: [],
};
}
);
Loading
Loading