Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5148a01
Add consol option to doAccounting Hardhat task
naddison36 Mar 17, 2026
c5b58fc
Changed doAccounting Action to use the ConsolidationController
naddison36 Mar 17, 2026
c66a8e6
Added getValidators Hardhat task
naddison36 Mar 17, 2026
13d9adf
Removed reference to BEACONCHAIN_API_KEY
naddison36 Mar 17, 2026
bc2cef2
More formatting of the output of the getValidators Hardhat task
naddison36 Mar 18, 2026
197a6fc
Added Claude and AGENTS md files
naddison36 Mar 18, 2026
02fa874
Added safe option to confirmConsol
naddison36 Mar 18, 2026
12b579d
Got getValidators working with dRPC
naddison36 Mar 19, 2026
b4aeb46
Added consol option to verifyDeposits which has to call snapBalances …
naddison36 Mar 19, 2026
ee03bd2
FIxed consolidation fork tests
naddison36 Mar 19, 2026
cb5299d
Deploy script to migrate the SSV clusters to ETH payment
naddison36 Mar 20, 2026
da64b82
Fix 183 deploy which was defaulting to Hoodi
naddison36 Mar 20, 2026
967e151
Updated Claude file
naddison36 Mar 20, 2026
e01a1ba
Merge remote-tracking branch 'origin/master' into nicka/consolidation…
naddison36 Mar 22, 2026
cde960f
Upgrade Lodestar dependencies
naddison36 Mar 23, 2026
fa61e48
Changed back to using GET for bulk validators
naddison36 Mar 24, 2026
f63672a
Merge remote-tracking branch 'origin/master' into nicka/consolidation…
naddison36 Apr 1, 2026
4be6d5c
Merge remote-tracking branch 'origin/master' into nicka/consolidation…
naddison36 Apr 30, 2026
9fcf6f2
Bumped deploy script number
naddison36 Apr 30, 2026
c28e625
Updated Claude file with SSV cluster payment details
naddison36 Apr 30, 2026
1723d04
Prettier
naddison36 Apr 30, 2026
9d63c6f
Merge remote-tracking branch 'origin/master' into nicka/consolidation…
naddison36 May 4, 2026
20e5fb7
Deployed 192_migrate_ssv_clusters_to_eth
naddison36 May 4, 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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Located in `contracts/strategies/`. Each strategy:
- Is registered with a vault and allocated collateral

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

### OTokens
`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.
Expand Down
78 changes: 78 additions & 0 deletions contracts/deploy/mainnet/192_migrate_ssv_clusters_to_eth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const { deploymentWithGovernanceProposal } = require("../../utils/deploy");
const { getClusterInfo, splitOperatorIds } = require("../../utils/ssv");

// ETH Payment estimator for SSV Clusters
// https://ssv-eth-forecasting.vercel.app/

const strategyConfigs = [
{
proxyName: "NativeStakingSSVStrategy2Proxy",
contractName: "NativeStakingSSVStrategy",
operatorids: "752,753,754,755",
// Another 63 validators will be migrated early May
// The remaining validators can be consolidated at the end of May
// 40 days with current number of validators should be more than enough
ethValue: 0.2498,
},
{
proxyName: "NativeStakingSSVStrategy3Proxy",
contractName: "NativeStakingSSVStrategy",
operatorids: "338,339,340,341",
// No ETH as this cluster no longer has any validators
// Need to upgrade to claim the SSV left in the cluster
ethValue: 0,
},
{
proxyName: "CompoundingStakingSSVStrategyProxy",
contractName: "CompoundingStakingSSVStrategy",
operatorids: "2070,2071,2072,2073",
// This gives 90 days operations with the current validator balances.
ethValue: 0.597136,
},
];

module.exports = deploymentWithGovernanceProposal(
{
deployName: "192_migrate_ssv_clusters_to_eth",
forceDeploy: false,
// forceSkip: true,
reduceQueueTime: true,
deployerIsProposer: false,
proposalId:
"27498848419509230881029695021341446610667421341841354940740078670919629788465",
},
async ({ ethers }) => {
const { chainId } = await ethers.provider.getNetwork();
const actions = [];

for (const strategyConfig of strategyConfigs) {
const proxy = await ethers.getContract(strategyConfig.proxyName);
const strategy = await ethers.getContractAt(
strategyConfig.contractName,
proxy.address
);
const operatorIds = splitOperatorIds(strategyConfig.operatorids);
const { cluster } = await getClusterInfo({
chainId,
operatorids: operatorIds.join(","),
ownerAddress: strategy.address,
});
const ethValue = strategyConfig.ethValue
? ethers.utils.parseEther(strategyConfig.ethValue.toString())
: 0;

actions.push({
contract: strategy,
signature:
"migrateClusterToETH(uint64[],(uint32,uint64,uint64,bool,uint256))",
args: [operatorIds, cluster],
value: ethValue,
});
}

return {
name: "Migrate SSV clusters to ETH billing for OETH staking strategies",
actions,
};
}
);
3 changes: 2 additions & 1 deletion contracts/deployments/mainnet/.migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@
"186_merkl_pb_bribes_module": 1773930008,
"188_harvesting_eip1271_oeth": 1774552963,
"189_harvesting_eip1271_ogn": 1774552993,
"190_remove_3rd_native_staking_strategy": 1775468903
"190_remove_3rd_native_staking_strategy": 1775468903,
"192_migrate_ssv_clusters_to_eth": 1777859891
}
13 changes: 11 additions & 2 deletions contracts/tasks/governance.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ const { sleep } = require("../utils/time.js");

async function execute(taskArguments, hre) {
const { isMainnet, isFork } = require("../test/helpers");
const { withConfirmation, impersonateGuardian } = require("../utils/deploy");
const {
withConfirmation,
impersonateGuardian,
getProposalExecutionValue,
} = require("../utils/deploy");

if (isMainnet) {
throw new Error("The execute task can not be used on mainnet");
Expand Down Expand Up @@ -54,7 +58,12 @@ async function execute(taskArguments, hre) {
if (isFork) {
// On the fork, impersonate the guardian and execute the proposal.
await impersonateGuardian();
await withConfirmation(governor.connect(sGuardian).execute(propId));
const executionValue = await getProposalExecutionValue(governor, propId);
await withConfirmation(
governor.connect(sGuardian).execute(propId, {
...(executionValue.gt(0) ? { value: executionValue } : {}),
})
);
} else {
// Localhost network. Execute as the governor account.
await governor.connect(sGovernor).execute(propId);
Expand Down
3 changes: 1 addition & 2 deletions contracts/tasks/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1107,8 +1107,7 @@ task("setRewardTokenAddresses", "Sets the reward token of a strategy")
task(
"updateWOETHPrice",
"Update the wOETH oracle price on the Base BridgedWOETHStrategy"
)
.setAction(updateWOETHOraclePrice);
).setAction(updateWOETHOraclePrice);

// Harvester

Expand Down
16 changes: 8 additions & 8 deletions contracts/utils/beacon.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ const getValidatorsIndividually = async (client, validatorIds, stateId) => {
return validators;
};

const getValidatorsByPost = async (
const getValidatorsByGet = async (
client,
validatorIds,
stateId,
Expand All @@ -237,31 +237,31 @@ const getValidatorsByPost = async (

for (let attempt = 1; attempt <= attempts; attempt++) {
try {
const postValidatorsRes = await client.beacon.postStateValidators({
const getValidatorsRes = await client.beacon.getStateValidators({
stateId,
validatorIds,
});

if (postValidatorsRes.ok) {
return postValidatorsRes.value();
if (getValidatorsRes.ok) {
return getValidatorsRes.value();
}

lastError = new Error(
`Bulk validator POST failed with status ${postValidatorsRes.status} ${postValidatorsRes.statusText}`
`Bulk validator GET failed with status ${getValidatorsRes.status} ${getValidatorsRes.statusText}`
);
log(`${lastError.message}. Attempt ${attempt} of ${attempts}.`);
} catch (err) {
lastError = err;
log(
`Bulk validator POST threw ${err.name || "Error"}: ${
`Bulk validator GET threw ${err.name || "Error"}: ${
err.message
}. Attempt ${attempt} of ${attempts}.`
);
}
}

if (lastError) {
log(`Bulk validator POST failed after ${attempts} attempts.`);
log(`Bulk validator GET failed after ${attempts} attempts.`);
}

return null;
Expand All @@ -274,7 +274,7 @@ const getValidators = async (pubkeys, stateId = "head") => {
log(
`Fetching ${validatorIds.length} validator details at state ${stateId} from the beacon node`
);
let validators = await getValidatorsByPost(client, validatorIds, stateId);
let validators = await getValidatorsByGet(client, validatorIds, stateId);

if (!validators) {
validators = await getValidatorsIndividually(client, validatorIds, stateId);
Expand Down
44 changes: 35 additions & 9 deletions contracts/utils/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ const _verifyProxyInitializedWithCorrectGovernor = (transactionData) => {
return;
}

if (isMainnet || isBase || isFork || isBaseFork) {
if (isMainnet || isBase || isFork) {
// TODO: Skip verification for Fork for now
return;
}
Expand Down Expand Up @@ -606,7 +606,10 @@ const executeGovernanceProposalOnFork = async ({
// Don't ask me why but this seems to force hardhat to
// update state and cause the random failures to stop
await getProposalState(proposalIdBn);
await governorSix.getActions(proposalIdBn);
const executionValue = await getProposalExecutionValue(
governorSix,
proposalIdBn
);

executionRetries = executionRetries - 1;
try {
Expand All @@ -615,6 +618,7 @@ const executeGovernanceProposalOnFork = async ({
"execute(uint256)"
](proposalIdBn, {
gasLimit: executeGasLimit || undefined,
...(executionValue.gt(0) ? { value: executionValue } : {}),
});
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -974,30 +978,47 @@ async function buildGnosisSafeJson(
safeAddress,
targets,
contractMethods,
contractInputsValues
contractInputsValues,
values = []
) {
return buildSafeTransactionBuilderJson({
safeAddress: safeAddress || addresses.mainnet.Guardian,
name: "Transaction Batch",
transactions: targets.map((target, i) => ({
to: target,
value: "0",
value: BigNumber.from(values[i] ?? 0).toString(),
contractMethod: contractMethods[i],
contractInputsValues: contractInputsValues[i],
})),
});
}

async function getProposalExecutionValue(governor, proposalId) {
const actions = await governor.getActions(proposalId);
const rawValues =
actions[1] || (typeof actions.values === "function" ? [] : actions.values);
const values = Array.from(rawValues || []);

return values.reduce(
(sum, value) => sum.add(BigNumber.from(value)),
BigNumber.from(0)
);
}

async function simulateWithTimelockImpersonation(proposal) {
log("Simulating the proposal directly on the timelock...");
const { timelockAddr } = await getNamedAccounts();
const timelock = await impersonateAndFund(timelockAddr);

for (const action of proposal.actions) {
const { contract, signature, args } = action;
const { contract, signature, args, value } = action;
const txOpts = {
...(await getTxOpts()),
...(value ? { value } : {}),
};

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

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

const guardianActions = [];
for (const action of proposal.actions) {
const { contract, signature, args } = action;
const { contract, signature, args, value } = action;
const txOpts = {
...(await getTxOpts()),
...(value ? { value } : {}),
};

log(`Sending governance action ${signature} to ${contract.address}`);
const result = await withConfirmation(
contract.connect(sGuardian)[signature](...args, await getTxOpts())
contract.connect(sGuardian)[signature](...args, txOpts)
);
guardianActions.push({
sig: signature,
args: args,
to: contract.address,
data: result.data,
value: result.value.toString(),
value: BigNumber.from(value ?? result.value ?? 0).toString(),
});

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

constructContractMethod,
buildGnosisSafeJson,
getProposalExecutionValue,

encodeSaltForCreateX,
createPoolBoosterSonic,
Expand Down
2 changes: 1 addition & 1 deletion contracts/utils/governor.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async function proposeGovernanceArgs(governorArgsArray) {

return [
targets,
Array(governorArgsArray.length).fill(BigNumber.from(0)),
governorArgsArray.map((action) => BigNumber.from(action.value ?? 0)),
sigs,
calldata,
];
Expand Down
Loading