Skip to content

Commit b2c852e

Browse files
authored
Portfolio APY for the Rebalancer (#2880)
* Change rebalancer config * Add portfolio APY * fix * update scripts * minor bug fix * Migrate to Talos
1 parent a29da20 commit b2c852e

7 files changed

Lines changed: 838 additions & 23 deletions

File tree

brownie/addresses.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@
211211
## Safe Modules
212212
ETHEREUM_BRIDGE_HELPER_MODULE = "0x630C1763D38AbE76301F58909fa174E7B84A7ECD"
213213
BASE_BRIDGE_HELPER_MODULE = "0xe3B3b4Fc77505EcfAACf6dD21619a8Cc12fcc501"
214+
PLUME_BRIDGE_HELPER_MODULE = "0xAc58C88349e00509FEc216E1B61d13b43315E18D"
214215

215216
## Morpho V2 (Base) Crosschain strategy
216217
CROSSCHAIN_MORPHO_V2_BASE_MASTER_STRATEGY = "0xB1d624fc40824683e2bFBEfd19eB208DbBE00866"

contracts/scripts/defender-actions/ousdRebalancer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const handler = async (event) => {
7979
warnings: plan.warnings,
8080
compact: true,
8181
baselineMarkets: plan.baselineMarkets,
82+
portfolioApy: plan.portfolioApy,
8283
});
8384
await postToDiscord(webhookUrl, `${header}\n\`\`\`\n${table}\n\`\`\``);
8485
} catch (err) {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/// <reference types="hardhat/types/runtime" />
2+
3+
import { ethers } from "ethers";
4+
import { formatUnits } from "ethers/lib/utils";
5+
import { types } from "hardhat/config";
6+
import addresses from "../../utils/addresses";
7+
import { logTxDetails } from "../../utils/txLogger";
8+
import { action } from "../lib/action";
9+
10+
// eslint-disable-next-line @typescript-eslint/no-var-requires
11+
const {
12+
buildRebalancePlan,
13+
formatAllocationTable,
14+
ACTION_DEPOSIT,
15+
ACTION_WITHDRAW,
16+
ACTION_NONE,
17+
} = require("../../utils/rebalancer");
18+
// eslint-disable-next-line @typescript-eslint/no-var-requires
19+
const { postToDiscord } = require("../../utils/discord");
20+
// eslint-disable-next-line @typescript-eslint/no-var-requires
21+
const { CROSS_CHAIN_BRIDGE_LIMIT } = require("../../utils/cctp");
22+
23+
const rebalancerModuleAbi = [
24+
"function processWithdrawalsAndDeposits(address[] calldata, uint256[] calldata, address[] calldata, uint256[] calldata) external",
25+
"function remainingDailyLimit() external view returns (uint256)",
26+
];
27+
28+
const cappedAmount = (rebalanceAction: any) => {
29+
const amount = rebalanceAction.delta.abs();
30+
return rebalanceAction.isCrossChain && amount.gt(CROSS_CHAIN_BRIDGE_LIMIT)
31+
? CROSS_CHAIN_BRIDGE_LIMIT
32+
: amount;
33+
};
34+
35+
const formatUsdc = (amount: any) => {
36+
const n = parseFloat(formatUnits(amount, 6));
37+
if (n >= 1e6) return `$${(n / 1e6).toFixed(2)}M`;
38+
if (n >= 1e3) return `$${(n / 1e3).toFixed(2)}K`;
39+
return `$${n.toFixed(2)}`;
40+
};
41+
42+
action({
43+
name: "ousdRebalancer",
44+
description:
45+
"Plan and execute OUSD strategy rebalancing via RebalancerModule",
46+
chains: [1],
47+
params: (t) => {
48+
t.addOptionalParam(
49+
"dryrun",
50+
"Compute and report plan without broadcasting transactions",
51+
false,
52+
types.boolean
53+
);
54+
t.addOptionalParam(
55+
"discordWebhook",
56+
"Override Discord webhook URL (falls back to DISCORD_WEBHOOK_URL env var)",
57+
undefined,
58+
types.string
59+
);
60+
},
61+
run: async ({ signer, args, log }) => {
62+
const dryrun = !!args.dryrun;
63+
const webhookUrl = args.discordWebhook || process.env.DISCORD_WEBHOOK_URL;
64+
65+
const plan = await buildRebalancePlan();
66+
const allActions = plan.actions;
67+
const actionable = allActions.filter((a: any) => a.action !== ACTION_NONE);
68+
69+
if (webhookUrl) {
70+
try {
71+
const timestamp = new Date().toUTCString().replace(/ GMT$/, " UTC");
72+
const dryTag = dryrun ? " `[DRY RUN]`" : "";
73+
const header = `🔄 **OUSD Rebalancer** — ${timestamp}${dryTag}`;
74+
const table = formatAllocationTable({
75+
actions: allActions,
76+
idealActions: plan.idealActions,
77+
vaultBalance: plan.state.vaultBalance,
78+
shortfall: plan.state.shortfall,
79+
warnings: plan.warnings,
80+
compact: true,
81+
baselineMarkets: plan.baselineMarkets,
82+
portfolioApy: plan.portfolioApy,
83+
});
84+
await postToDiscord(webhookUrl, `${header}\n\`\`\`\n${table}\n\`\`\``);
85+
} catch (err: any) {
86+
log.warn(`Discord notification failed: ${err?.message || String(err)}`);
87+
}
88+
}
89+
90+
if (actionable.length === 0) {
91+
log.info("No rebalancing actions required");
92+
return;
93+
}
94+
95+
const rebalancerModule = new ethers.Contract(
96+
addresses.mainnet.OUSDRebalancerModule,
97+
rebalancerModuleAbi,
98+
signer
99+
);
100+
101+
const totalPlanned = actionable.reduce(
102+
(sum: any, rebalanceAction: any) =>
103+
sum.add(cappedAmount(rebalanceAction)),
104+
ethers.BigNumber.from(0)
105+
);
106+
107+
const remaining = dryrun
108+
? ethers.constants.MaxUint256
109+
: await rebalancerModule.remainingDailyLimit();
110+
111+
if (totalPlanned.gt(remaining)) {
112+
const warning =
113+
`Daily limit too low for batch: need ${formatUsdc(totalPlanned)}, ` +
114+
`remaining ${formatUsdc(remaining)}`;
115+
log.warn(warning);
116+
if (webhookUrl) {
117+
await postToDiscord(
118+
webhookUrl,
119+
`⚠️ **OUSD Rebalancer** — Planned movement ${formatUsdc(
120+
totalPlanned
121+
)} exceeds remaining daily limit ${formatUsdc(
122+
remaining
123+
)}, skipping execution`
124+
).catch((err: any) =>
125+
log.warn(
126+
`Discord warning post failed: ${err?.message || String(err)}`
127+
)
128+
);
129+
}
130+
return;
131+
}
132+
133+
if (dryrun) {
134+
log.info("[DRY RUN] Skipping RebalancerModule transaction");
135+
return;
136+
}
137+
138+
const withdrawals = actionable.filter(
139+
(a: any) => a.action === ACTION_WITHDRAW
140+
);
141+
const deposits = actionable.filter((a: any) => a.action === ACTION_DEPOSIT);
142+
143+
const hasCrossChainWithdrawal = withdrawals.some(
144+
(a: any) => a.isCrossChain
145+
);
146+
const deferDeposits = hasCrossChainWithdrawal;
147+
148+
const tx = await rebalancerModule.processWithdrawalsAndDeposits(
149+
withdrawals.map((a: any) => a.address),
150+
withdrawals.map(cappedAmount),
151+
deferDeposits ? [] : deposits.map((a: any) => a.address),
152+
deferDeposits ? [] : deposits.map(cappedAmount)
153+
);
154+
await logTxDetails(tx, "OUSD rebalance");
155+
156+
if (webhookUrl) {
157+
await postToDiscord(
158+
webhookUrl,
159+
`✅ OUSD rebalance tx sent: \`${tx.hash}\``
160+
).catch((err: any) =>
161+
log.warn(`Discord tx hash post failed: ${err?.message || String(err)}`)
162+
);
163+
}
164+
},
165+
});

0 commit comments

Comments
 (0)