Skip to content

Commit bc044c5

Browse files
authored
Update of SSV clusters to use ETH payment (#2860)
* Add consol option to doAccounting Hardhat task * Changed doAccounting Action to use the ConsolidationController * Added getValidators Hardhat task Refactored getting Beacon chain data to use Lodestar. So removed Beaconcha.in API use * Removed reference to BEACONCHAIN_API_KEY Formatting of getValidators * More formatting of the output of the getValidators Hardhat task Added epoch option to getValidators Hardhat task * Added Claude and AGENTS md files * Added safe option to confirmConsol Refactored writing Safe file into safe util * Got getValidators working with dRPC * Added consol option to verifyDeposits which has to call snapBalances on the ConsolidationController * FIxed consolidation fork tests * Deploy script to migrate the SSV clusters to ETH payment * Fix 183 deploy which was defaulting to Hoodi * Updated Claude file * Upgrade Lodestar dependencies * Changed back to using GET for bulk validators * Bumped deploy script number * Updated Claude file with SSV cluster payment details * Prettier * Deployed 192_migrate_ssv_clusters_to_eth
1 parent 07bbede commit bc044c5

7 files changed

Lines changed: 129 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Located in `contracts/strategies/`. Each strategy:
107107
- Is registered with a vault and allocated collateral
108108

109109
Key strategies: Aave, Compound, Convex/Curve, Balancer, Morpho, Native Staking (SSV validators).
110+
- For SSV Cluster migrations to ETH billing, use the SSV ETH payment calculator: https://ssv-eth-forecasting.vercel.app/
110111

111112
### OTokens
112113
`contracts/token/OUSD.sol` and `contracts/token/OETH.sol` - rebasing ERC-20 tokens. OUSD rebases to all holders; OETH uses a similar mechanism for ETH-denominated yield.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const { deploymentWithGovernanceProposal } = require("../../utils/deploy");
2+
const { getClusterInfo, splitOperatorIds } = require("../../utils/ssv");
3+
4+
// ETH Payment estimator for SSV Clusters
5+
// https://ssv-eth-forecasting.vercel.app/
6+
7+
const strategyConfigs = [
8+
{
9+
proxyName: "NativeStakingSSVStrategy2Proxy",
10+
contractName: "NativeStakingSSVStrategy",
11+
operatorids: "752,753,754,755",
12+
// Another 63 validators will be migrated early May
13+
// The remaining validators can be consolidated at the end of May
14+
// 40 days with current number of validators should be more than enough
15+
ethValue: 0.2498,
16+
},
17+
{
18+
proxyName: "NativeStakingSSVStrategy3Proxy",
19+
contractName: "NativeStakingSSVStrategy",
20+
operatorids: "338,339,340,341",
21+
// No ETH as this cluster no longer has any validators
22+
// Need to upgrade to claim the SSV left in the cluster
23+
ethValue: 0,
24+
},
25+
{
26+
proxyName: "CompoundingStakingSSVStrategyProxy",
27+
contractName: "CompoundingStakingSSVStrategy",
28+
operatorids: "2070,2071,2072,2073",
29+
// This gives 90 days operations with the current validator balances.
30+
ethValue: 0.597136,
31+
},
32+
];
33+
34+
module.exports = deploymentWithGovernanceProposal(
35+
{
36+
deployName: "192_migrate_ssv_clusters_to_eth",
37+
forceDeploy: false,
38+
// forceSkip: true,
39+
reduceQueueTime: true,
40+
deployerIsProposer: false,
41+
proposalId:
42+
"27498848419509230881029695021341446610667421341841354940740078670919629788465",
43+
},
44+
async ({ ethers }) => {
45+
const { chainId } = await ethers.provider.getNetwork();
46+
const actions = [];
47+
48+
for (const strategyConfig of strategyConfigs) {
49+
const proxy = await ethers.getContract(strategyConfig.proxyName);
50+
const strategy = await ethers.getContractAt(
51+
strategyConfig.contractName,
52+
proxy.address
53+
);
54+
const operatorIds = splitOperatorIds(strategyConfig.operatorids);
55+
const { cluster } = await getClusterInfo({
56+
chainId,
57+
operatorids: operatorIds.join(","),
58+
ownerAddress: strategy.address,
59+
});
60+
const ethValue = strategyConfig.ethValue
61+
? ethers.utils.parseEther(strategyConfig.ethValue.toString())
62+
: 0;
63+
64+
actions.push({
65+
contract: strategy,
66+
signature:
67+
"migrateClusterToETH(uint64[],(uint32,uint64,uint64,bool,uint256))",
68+
args: [operatorIds, cluster],
69+
value: ethValue,
70+
});
71+
}
72+
73+
return {
74+
name: "Migrate SSV clusters to ETH billing for OETH staking strategies",
75+
actions,
76+
};
77+
}
78+
);

contracts/deployments/mainnet/.migrations.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,6 @@
7777
"186_merkl_pb_bribes_module": 1773930008,
7878
"188_harvesting_eip1271_oeth": 1774552963,
7979
"189_harvesting_eip1271_ogn": 1774552993,
80-
"190_remove_3rd_native_staking_strategy": 1775468903
80+
"190_remove_3rd_native_staking_strategy": 1775468903,
81+
"192_migrate_ssv_clusters_to_eth": 1777859891
8182
}

contracts/tasks/governance.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ const { sleep } = require("../utils/time.js");
22

33
async function execute(taskArguments, hre) {
44
const { isMainnet, isFork } = require("../test/helpers");
5-
const { withConfirmation, impersonateGuardian } = require("../utils/deploy");
5+
const {
6+
withConfirmation,
7+
impersonateGuardian,
8+
getProposalExecutionValue,
9+
} = require("../utils/deploy");
610

711
if (isMainnet) {
812
throw new Error("The execute task can not be used on mainnet");
@@ -54,7 +58,12 @@ async function execute(taskArguments, hre) {
5458
if (isFork) {
5559
// On the fork, impersonate the guardian and execute the proposal.
5660
await impersonateGuardian();
57-
await withConfirmation(governor.connect(sGuardian).execute(propId));
61+
const executionValue = await getProposalExecutionValue(governor, propId);
62+
await withConfirmation(
63+
governor.connect(sGuardian).execute(propId, {
64+
...(executionValue.gt(0) ? { value: executionValue } : {}),
65+
})
66+
);
5867
} else {
5968
// Localhost network. Execute as the governor account.
6069
await governor.connect(sGovernor).execute(propId);

contracts/tasks/tasks.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,8 +1107,7 @@ task("setRewardTokenAddresses", "Sets the reward token of a strategy")
11071107
task(
11081108
"updateWOETHPrice",
11091109
"Update the wOETH oracle price on the Base BridgedWOETHStrategy"
1110-
)
1111-
.setAction(updateWOETHOraclePrice);
1110+
).setAction(updateWOETHOraclePrice);
11121111

11131112
// Harvester
11141113

contracts/utils/deploy.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ const _verifyProxyInitializedWithCorrectGovernor = (transactionData) => {
338338
return;
339339
}
340340

341-
if (isMainnet || isBase || isFork || isBaseFork) {
341+
if (isMainnet || isBase || isFork) {
342342
// TODO: Skip verification for Fork for now
343343
return;
344344
}
@@ -606,7 +606,10 @@ const executeGovernanceProposalOnFork = async ({
606606
// Don't ask me why but this seems to force hardhat to
607607
// update state and cause the random failures to stop
608608
await getProposalState(proposalIdBn);
609-
await governorSix.getActions(proposalIdBn);
609+
const executionValue = await getProposalExecutionValue(
610+
governorSix,
611+
proposalIdBn
612+
);
610613

611614
executionRetries = executionRetries - 1;
612615
try {
@@ -615,6 +618,7 @@ const executeGovernanceProposalOnFork = async ({
615618
"execute(uint256)"
616619
](proposalIdBn, {
617620
gasLimit: executeGasLimit || undefined,
621+
...(executionValue.gt(0) ? { value: executionValue } : {}),
618622
});
619623
} catch (e) {
620624
console.error(e);
@@ -974,30 +978,47 @@ async function buildGnosisSafeJson(
974978
safeAddress,
975979
targets,
976980
contractMethods,
977-
contractInputsValues
981+
contractInputsValues,
982+
values = []
978983
) {
979984
return buildSafeTransactionBuilderJson({
980985
safeAddress: safeAddress || addresses.mainnet.Guardian,
981986
name: "Transaction Batch",
982987
transactions: targets.map((target, i) => ({
983988
to: target,
984-
value: "0",
989+
value: BigNumber.from(values[i] ?? 0).toString(),
985990
contractMethod: contractMethods[i],
986991
contractInputsValues: contractInputsValues[i],
987992
})),
988993
});
989994
}
990995

996+
async function getProposalExecutionValue(governor, proposalId) {
997+
const actions = await governor.getActions(proposalId);
998+
const rawValues =
999+
actions[1] || (typeof actions.values === "function" ? [] : actions.values);
1000+
const values = Array.from(rawValues || []);
1001+
1002+
return values.reduce(
1003+
(sum, value) => sum.add(BigNumber.from(value)),
1004+
BigNumber.from(0)
1005+
);
1006+
}
1007+
9911008
async function simulateWithTimelockImpersonation(proposal) {
9921009
log("Simulating the proposal directly on the timelock...");
9931010
const { timelockAddr } = await getNamedAccounts();
9941011
const timelock = await impersonateAndFund(timelockAddr);
9951012

9961013
for (const action of proposal.actions) {
997-
const { contract, signature, args } = action;
1014+
const { contract, signature, args, value } = action;
1015+
const txOpts = {
1016+
...(await getTxOpts()),
1017+
...(value ? { value } : {}),
1018+
};
9981019

9991020
log(`Sending governance action ${signature} to ${contract.address}`);
1000-
await contract.connect(timelock)[signature](...args, await getTxOpts());
1021+
await contract.connect(timelock)[signature](...args, txOpts);
10011022

10021023
console.log(`... ${signature} completed`);
10031024
}
@@ -1232,18 +1253,22 @@ function deploymentWithGuardianGovernor(opts, fn) {
12321253

12331254
const guardianActions = [];
12341255
for (const action of proposal.actions) {
1235-
const { contract, signature, args } = action;
1256+
const { contract, signature, args, value } = action;
1257+
const txOpts = {
1258+
...(await getTxOpts()),
1259+
...(value ? { value } : {}),
1260+
};
12361261

12371262
log(`Sending governance action ${signature} to ${contract.address}`);
12381263
const result = await withConfirmation(
1239-
contract.connect(sGuardian)[signature](...args, await getTxOpts())
1264+
contract.connect(sGuardian)[signature](...args, txOpts)
12401265
);
12411266
guardianActions.push({
12421267
sig: signature,
12431268
args: args,
12441269
to: contract.address,
12451270
data: result.data,
1246-
value: result.value.toString(),
1271+
value: BigNumber.from(value ?? result.value ?? 0).toString(),
12471272
});
12481273

12491274
console.log(`... ${signature} completed`);
@@ -1449,6 +1474,7 @@ module.exports = {
14491474

14501475
constructContractMethod,
14511476
buildGnosisSafeJson,
1477+
getProposalExecutionValue,
14521478

14531479
encodeSaltForCreateX,
14541480
createPoolBoosterSonic,

contracts/utils/governor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async function proposeGovernanceArgs(governorArgsArray) {
4747

4848
return [
4949
targets,
50-
Array(governorArgsArray.length).fill(BigNumber.from(0)),
50+
governorArgsArray.map((action) => BigNumber.from(action.value ?? 0)),
5151
sigs,
5252
calldata,
5353
];

0 commit comments

Comments
 (0)