Skip to content

Commit 19b7799

Browse files
committed
Merge origin/master into talos
2 parents 38e7664 + 78e4669 commit 19b7799

29 files changed

Lines changed: 7179 additions & 708 deletions

contracts/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,12 @@ pnpm hardhat setActionVars --id b1d831f1-29d4-4943-bb2e-8e625b76e82c
398398
pnpm hardhat setActionVars --id 6567d7c6-7ec7-44bd-b95b-470dd1ff780b
399399
pnpm hardhat setActionVars --id 6a633bb0-aff8-4b37-aaae-b4c6f244ed87
400400
pnpm hardhat setActionVars --id 076c59e4-4150-42c7-9ba0-9962069ac353
401+
pnpm hardhat setActionVars --id ca80b55c-f3f7-4e03-a2f5-ea444645f8d9
401402
pnpm hardhat setActionVars --id f74f24b4-d98b-4181-89cc-6608369b6f91
402403
pnpm hardhat setActionVars --id aa194c13-0dbf-49d2-8e87-70e61f3d71a8
403404
pnpm hardhat setActionVars --id 65b53496-e426-4850-8349-059e63eb2120
404405
pnpm hardhat setActionVars --id a4f8ca5f-7144-469b-b84a-58b30fed72ce
406+
pnpm hardhat setActionVars --id cbddb98e-a0f6-4aa8-bd73-20aa8e81d704
405407

406408
# Mainnet
407409
pnpm hardhat updateAction --id e2929f53-db56-49b2-b054-35f7df7fc4fb --file doAccounting
@@ -412,6 +414,7 @@ pnpm hardhat updateAction --id b1d831f1-29d4-4943-bb2e-8e625b76e82c --file claim
412414
pnpm hardhat updateAction --id 6567d7c6-7ec7-44bd-b95b-470dd1ff780b --file manageBribeOnSonic
413415
pnpm hardhat updateAction --id 6a633bb0-aff8-4b37-aaae-b4c6f244ed87 --file managePassThrough
414416
pnpm hardhat updateAction --id 076c59e4-4150-42c7-9ba0-9962069ac353 --file manageBribes
417+
pnpm hardhat updateAction --id ca80b55c-f3f7-4e03-a2f5-ea444645f8d9 --file ousdRebalancer
415418
pnpm hardhat updateAction --id f74f24b4-d98b-4181-89cc-6608369b6f91 --file updateVotemarketEpochs
416419
pnpm hardhat updateAction --id aa194c13-0dbf-49d2-8e87-70e61f3d71a8 --file manageMerklBribes # Mainnet
417420
pnpm hardhat updateAction --id 65b53496-e426-4850-8349-059e63eb2120 --file manageMerklBribes # Base
@@ -423,12 +426,29 @@ pnpm hardhat updateAction --id a4f8ca5f-7144-469b-b84a-58b30fed72ce --file claim
423426
pnpm hardhat updateAction --id bb43e5da-f936-4185-84da-253394583665 --file crossChainRelay
424427
pnpm hardhat updateAction --id e571409b-5399-48e4-bfb2-50b7af9903aa --file crossChainRelay
425428

429+
# One-off action for backfilling a missing tx-level key in Defender KV store
430+
pnpm hardhat updateAction --id cbddb98e-a0f6-4aa8-bd73-20aa8e81d704 --file crossChainRelayBackfillTx
431+
426432
# OUSD Ethereum -> HyperEVM actions
427433
pnpm hardhat updateAction --id 0b852456-96a0-4f1d-9d6c-39e1c6ae9dfc --file crossChainRelayHyperEVM
428434
pnpm hardhat updateAction --id 65f04f74-8da7-4fc5-94b3-96be31bac03b --file crossChainRelayHyperEVM
429435

430436
```
431437

438+
To backfill a missing tx-level cross-chain relay key, set
439+
`HARDCODED_TRANSACTION_ID` and `HARDCODED_DESTINATION_CHAIN_ID` in
440+
`scripts/defender-actions/crossChainRelayBackfillTx.js`, then bundle/upload and
441+
run the `crossChainRelayBackfillTx` Action manually in Defender.
442+
443+
This stores both tx-level keys with value `processed` in Defender's key-value
444+
store:
445+
446+
- `cctp_message_<HARDCODED_TRANSACTION_ID>` (legacy format)
447+
- `cctp_message_<HARDCODED_TRANSACTION_ID>_<cctpDestinationDomainId>` (current
448+
format from `tasks/crossChain.js`, where domain id is derived from the
449+
hardcoded destination chain id: mainnet `1 -> 0`, base `8453 -> 6`, hyperevm
450+
`999 -> 19`)
451+
432452
`rollup` can be installed globally to avoid the `npx` prefix.
433453

434454
### Encrypting / decrypting validator private keys
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import { AbstractSafeModule } from "./AbstractSafeModule.sol";
5+
6+
import { IVault } from "../interfaces/IVault.sol";
7+
import { VaultStorage } from "../vault/VaultStorage.sol";
8+
9+
/**
10+
* @title Rebalancer Module
11+
* @notice A Gnosis Safe module that automates OUSD vault rebalancing by
12+
* withdrawing from overallocated strategies and depositing to
13+
* underallocated strategies.
14+
*
15+
* @dev The Safe (Guardian multisig) must:
16+
* 1. Deploy this module
17+
* 2. Call `safe.enableModule(address(this))` to authorize it
18+
*
19+
* An off-chain operator (e.g. Defender Action) calls
20+
* `processWithdrawalsAndDeposits` periodically with computed strategy/amount
21+
* arrays. Either array may be empty. All intelligence (APY fetching, target
22+
* allocation, constraint enforcement) lives off-chain. This contract is a
23+
* dumb executor.
24+
*
25+
* The function uses soft failures: if a single strategy call fails via the
26+
* Safe, the module emits an event and continues to the next strategy rather
27+
* than reverting the entire batch.
28+
*
29+
* The Safe retains full control via `setPaused`.
30+
*/
31+
contract RebalancerModule is AbstractSafeModule {
32+
// ───────────────────────────────────────────────────────── Immutables ──
33+
34+
/// @notice The vault whose strategies are being rebalanced.
35+
IVault public immutable vault;
36+
37+
/// @notice The vault's base asset (e.g. USDC for OUSD).
38+
address public immutable asset;
39+
40+
// ────────────────────────────────────────────────────── Mutable config ──
41+
42+
/// @notice When true, processWithdrawalsAndDeposits is blocked.
43+
bool public paused;
44+
45+
/// @notice Strategies that this module is permitted to withdraw from or deposit into.
46+
mapping(address => bool) public isAllowedStrategy;
47+
48+
/// @notice Cumulative amount moved (withdrawals + deposits) per calendar day.
49+
/// Day key = block.timestamp / 1 days (i.e. days since Unix epoch).
50+
mapping(uint256 => uint256) public amountMovedPerDay;
51+
52+
/// @notice Max percentage of vault TVL that can be moved in a single day.
53+
/// In basis points (e.g. 20000 = 200%).
54+
uint256 public maxDailyMovementBps;
55+
56+
// ─────────────────────────────────────────────────────────── Events ──
57+
58+
/// @notice Emitted after processWithdrawals completes (even if some failed).
59+
event WithdrawalsProcessed(
60+
address[] strategies,
61+
uint256[] amounts,
62+
uint256 remainingShortfall
63+
);
64+
65+
/// @notice Emitted after processDeposits completes (even if some failed).
66+
event DepositsProcessed(address[] strategies, uint256[] amounts);
67+
68+
/// @notice Emitted when a single withdrawFromStrategy call fails via the Safe.
69+
event WithdrawalFailed(address indexed strategy, uint256 attemptedAmount);
70+
71+
/// @notice Emitted when a single depositToStrategy call fails via the Safe.
72+
event DepositFailed(address indexed strategy, uint256 attemptedAmount);
73+
74+
/// @notice Emitted when the paused state changes.
75+
event PausedStateChanged(bool paused);
76+
77+
/// @notice Emitted when a strategy is added to the whitelist.
78+
event StrategyAllowed(address indexed strategy);
79+
80+
/// @notice Emitted when a strategy is removed from the whitelist.
81+
event StrategyRevoked(address indexed strategy);
82+
83+
/// @notice Emitted when the daily movement limit is updated.
84+
event MaxDailyMovementBpsSet(uint256 maxDailyMovementBps);
85+
86+
// ─────────────────────────────────────────────────────── Constructor ──
87+
88+
/**
89+
* @param _safeContract Address of the Gnosis Safe (Guardian multisig).
90+
* @param _operator Address of the off-chain operator (e.g. Defender relayer).
91+
* @param _vault Address of the OUSD vault.
92+
*/
93+
constructor(
94+
address _safeContract,
95+
address _operator,
96+
address _vault
97+
) AbstractSafeModule(_safeContract) {
98+
require(_vault != address(0), "Invalid vault");
99+
100+
vault = IVault(_vault);
101+
asset = IVault(_vault).asset();
102+
maxDailyMovementBps = 20000; // 200%
103+
104+
_grantRole(OPERATOR_ROLE, _operator);
105+
}
106+
107+
// ──────────────────────────────────────────────────────── Modifiers ──
108+
109+
modifier whenNotPaused() {
110+
require(!paused, "Module is paused");
111+
_;
112+
}
113+
114+
// ──────────────────────────────────────────────── Core automation ──
115+
116+
// slither-disable-start reentrancy-no-eth
117+
/**
118+
* @notice Withdraw from overallocated strategies then deposit to underallocated
119+
* ones. Either array may be empty — the contract loops over zero entries
120+
* without reverting.
121+
*
122+
* @param _withdrawStrategies Strategies to withdraw from.
123+
* @param _withdrawAmounts Amounts to withdraw from each strategy.
124+
* @param _depositStrategies Strategies to deposit into.
125+
* @param _depositAmounts Amounts to deposit into each strategy.
126+
*/
127+
function processWithdrawalsAndDeposits(
128+
address[] calldata _withdrawStrategies,
129+
uint256[] calldata _withdrawAmounts,
130+
address[] calldata _depositStrategies,
131+
uint256[] calldata _depositAmounts
132+
) external onlyOperator whenNotPaused {
133+
require(
134+
_withdrawStrategies.length == _withdrawAmounts.length,
135+
"Withdraw array length mismatch"
136+
);
137+
require(
138+
_depositStrategies.length == _depositAmounts.length,
139+
"Deposit array length mismatch"
140+
);
141+
// This is a permissionless call; no Safe exec needed.
142+
vault.addWithdrawalQueueLiquidity();
143+
uint256 _limit = dailyLimit();
144+
_executeWithdrawals(_withdrawStrategies, _withdrawAmounts, _limit);
145+
_executeDeposits(_depositStrategies, _depositAmounts, _limit);
146+
emit WithdrawalsProcessed(
147+
_withdrawStrategies,
148+
_withdrawAmounts,
149+
pendingShortfall()
150+
);
151+
emit DepositsProcessed(_depositStrategies, _depositAmounts);
152+
}
153+
154+
// ─────────────────────────────────────── Guardian controls ──
155+
156+
/**
157+
* @notice Pause or unpause the module.
158+
* @param _paused True to pause, false to unpause.
159+
*/
160+
function setPaused(bool _paused) external onlySafe {
161+
paused = _paused;
162+
emit PausedStateChanged(_paused);
163+
}
164+
165+
/**
166+
* @notice Add a strategy to the whitelist, allowing the operator to move
167+
* funds into or out of it.
168+
* @param _strategy Strategy address to allow.
169+
*/
170+
function allowStrategy(address _strategy) external onlySafe {
171+
require(_strategy != address(0), "Invalid strategy");
172+
isAllowedStrategy[_strategy] = true;
173+
emit StrategyAllowed(_strategy);
174+
}
175+
176+
/**
177+
* @notice Remove a strategy from the whitelist.
178+
* @param _strategy Strategy address to revoke.
179+
*/
180+
function revokeStrategy(address _strategy) external onlySafe {
181+
isAllowedStrategy[_strategy] = false;
182+
emit StrategyRevoked(_strategy);
183+
}
184+
185+
/**
186+
* @notice Set the maximum percentage of vault TVL that can be moved per day.
187+
* @param _maxDailyMovementBps Limit in basis points (e.g. 20000 = 200%).
188+
* Set to 0 for unlimited daily movement.
189+
*/
190+
function setMaxDailyMovementBps(uint256 _maxDailyMovementBps)
191+
external
192+
onlySafe
193+
{
194+
maxDailyMovementBps = _maxDailyMovementBps;
195+
emit MaxDailyMovementBpsSet(_maxDailyMovementBps);
196+
}
197+
198+
// ──────────────────────────────────────────────────────── View helpers ──
199+
200+
/**
201+
* @notice The current unmet shortfall in the vault's withdrawal queue.
202+
* @dev This is a raw read of `queued - claimable`. It does NOT account for
203+
* idle vault asset that `addWithdrawalQueueLiquidity()` would absorb.
204+
* For a fully up-to-date figure, call `vault.addWithdrawalQueueLiquidity()`
205+
* first (which is what `processWithdrawals` does).
206+
* @return shortfall Queue shortfall in asset units (vault asset decimals).
207+
*/
208+
function pendingShortfall() public view returns (uint256 shortfall) {
209+
VaultStorage.WithdrawalQueueMetadata memory meta = vault
210+
.withdrawalQueueMetadata();
211+
shortfall = meta.queued - meta.claimable;
212+
}
213+
214+
/**
215+
* @notice The daily movement limit based on current vault TVL.
216+
* @dev vault.totalValue() includes AMO (Automated Market Operations)
217+
* value. Excluding AMO would add significant complexity for minimal
218+
* accuracy gain, so the limit is slightly more generous than intended.
219+
* Additionally, if the vault's TVL changes significantly mid-day (e.g.
220+
* large mint/redeem), the limit will reflect the TVL at call time —
221+
* this is acceptable since the limit is a safety backstop, not a
222+
* precise cap.
223+
* If maxDailyMovementBps is set to 0, this returns type(uint256).max
224+
* as a sentinel value to represent an unlimited cap.
225+
* @return limit Amount in asset units (vault asset decimals).
226+
*/
227+
function dailyLimit() public view returns (uint256 limit) {
228+
if (maxDailyMovementBps == 0) {
229+
return type(uint256).max;
230+
}
231+
limit = (vault.totalValue() * maxDailyMovementBps) / 10000;
232+
}
233+
234+
/**
235+
* @notice The remaining amount that can be moved today before hitting the
236+
* daily movement limit.
237+
* @return remaining Amount in asset units (vault asset decimals).
238+
*/
239+
function remainingDailyLimit() public view returns (uint256 remaining) {
240+
uint256 limit = dailyLimit();
241+
uint256 used = amountMovedPerDay[block.timestamp / 1 days];
242+
remaining = used >= limit ? 0 : limit - used;
243+
}
244+
245+
// ──────────────────────────────────────────────── Internal helpers ──
246+
247+
/// @dev Track cumulative daily movement and revert if the limit is exceeded.
248+
function _trackMovement(uint256 _amount, uint256 _dailyLimit) internal {
249+
uint256 dayKey = block.timestamp / 1 days;
250+
amountMovedPerDay[dayKey] += _amount;
251+
252+
require(
253+
amountMovedPerDay[dayKey] <= _dailyLimit,
254+
"Daily movement limit exceeded"
255+
);
256+
}
257+
258+
/// @dev Execute withdrawFromStrategy for each (strategy, amount) pair via the Safe.
259+
function _executeWithdrawals(
260+
address[] calldata _strategies,
261+
uint256[] calldata _amounts,
262+
uint256 _dailyLimit
263+
) internal {
264+
address[] memory assets = _toAddressArray(asset);
265+
for (uint256 i = 0; i < _strategies.length; i++) {
266+
if (_amounts[i] == 0) continue;
267+
require(isAllowedStrategy[_strategies[i]], "Strategy not allowed");
268+
bool success = safeContract.execTransactionFromModule(
269+
address(vault),
270+
0,
271+
abi.encodeWithSelector(
272+
IVault.withdrawFromStrategy.selector,
273+
_strategies[i],
274+
assets,
275+
_toUint256Array(_amounts[i])
276+
),
277+
0
278+
);
279+
if (success) {
280+
_trackMovement(_amounts[i], _dailyLimit);
281+
} else {
282+
emit WithdrawalFailed(_strategies[i], _amounts[i]);
283+
}
284+
}
285+
}
286+
287+
/// @dev Execute depositToStrategy for each (strategy, amount) pair via the Safe.
288+
function _executeDeposits(
289+
address[] calldata _strategies,
290+
uint256[] calldata _amounts,
291+
uint256 _dailyLimit
292+
) internal {
293+
address[] memory assets = _toAddressArray(asset);
294+
for (uint256 i = 0; i < _strategies.length; i++) {
295+
if (_amounts[i] == 0) continue;
296+
require(isAllowedStrategy[_strategies[i]], "Strategy not allowed");
297+
bool success = safeContract.execTransactionFromModule(
298+
address(vault),
299+
0,
300+
abi.encodeWithSelector(
301+
IVault.depositToStrategy.selector,
302+
_strategies[i],
303+
assets,
304+
_toUint256Array(_amounts[i])
305+
),
306+
0
307+
);
308+
if (success) {
309+
_trackMovement(_amounts[i], _dailyLimit);
310+
} else {
311+
emit DepositFailed(_strategies[i], _amounts[i]);
312+
}
313+
}
314+
}
315+
316+
// slither-disable-end reentrancy-no-eth
317+
318+
function _toAddressArray(address _addr)
319+
internal
320+
pure
321+
returns (address[] memory arr)
322+
{
323+
arr = new address[](1);
324+
arr[0] = _addr;
325+
}
326+
327+
function _toUint256Array(uint256 _val)
328+
internal
329+
pure
330+
returns (uint256[] memory arr)
331+
{
332+
arr = new uint256[](1);
333+
arr[0] = _val;
334+
}
335+
}

0 commit comments

Comments
 (0)