Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ contracts/coverage/
contracts/coverage.json
contracts/build/
contracts/dist/
contracts/.localKeyValueStorage
contracts/.localKeyValueStorageMainnet
contracts/.localKeyValueStorageHolesky

todo.txt
brownie/env-brownie/
Expand Down
3 changes: 2 additions & 1 deletion contracts/deploy/deployActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,8 @@ const deployOETHHarvester = async (oethDripper) => {

await withConfirmation(
// prettier-ignore
cOETHHarvesterProxy["initialize(address,address,bytes)"](
cOETHHarvesterProxy
["initialize(address,address,bytes)"](
dOETHHarvester.address,
governorAddr,
[]
Expand Down
31 changes: 31 additions & 0 deletions contracts/deploy/holesky/006_update_registrator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { parseEther } = require("ethers/lib/utils");

const { deployNativeStakingSSVStrategy } = require("../deployActions");
const { withConfirmation } = require("../../utils/deploy");
const { resolveContract } = require("../../utils/resolvers");
const addresses = require("../../utils/addresses");

const mainExport = async () => {
console.log("Running 006 deployment on Holesky...");

const cNativeStakingStrategy = await resolveContract(
"NativeStakingSSVStrategyProxy",
"NativeStakingSSVStrategy"
);

await withConfirmation(
cNativeStakingStrategy
// Holesky defender relayer
.setRegistrator(addresses.holesky.validatorRegistrator)
);

console.log("Running 006 deployment done");
return true;
};

mainExport.id = "006_update_registrator";
mainExport.tags = [];
mainExport.dependencies = [];
mainExport.skip = () => false;

module.exports = mainExport;
3 changes: 2 additions & 1 deletion contracts/deployments/holesky/.migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"002_upgrade_strategy": 1714233842,
"003_deposit_to_native_strategy": 1714307581,
"004_upgrade_strategy": 1714944723,
"005_deploy_new_harvester": 1714998707
"005_deploy_new_harvester": 1714998707,
"006_update_registrator": 1715184342
}
4 changes: 4 additions & 0 deletions contracts/dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ ACCOUNTS_TO_FUND=
# need of running migration scripts.

# HOT_DEPLOY=strategy,vaultCore,vaultAdmin,harvester

#P2P API KEYS
P2P_MAINNET_API_KEY=[SET API Key]
P2P_HOLESKY_API_KEY=[SET API Key]
1 change: 1 addition & 0 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"axios": "^1.4.0",
"chai": "^4.3.4",
"debug": "^4.3.4",
"defender-kvstore-client": "^1.38.1-rc.0",
"dotenv": "^10.0.0",
"eslint": "^7.32.0",
"ethereum-waffle": "^4.0.10",
Expand Down
99 changes: 99 additions & 0 deletions contracts/tasks/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const { env } = require("./env");
const { execute, executeOnFork, proposal, governors } = require("./governance");
const { smokeTest, smokeTestCheck } = require("./smokeTest");
const addresses = require("../utils/addresses");
const { getDefenderSigner } = require("../utils/signers");
const { networkMap } = require("../utils/hardhat-helpers");
const { resolveContract } = require("../utils/resolvers");
const { KeyValueStoreClient } = require("defender-kvstore-client");
const { operateValidators } = require("./validator");
const { formatUnits } = require("ethers/lib/utils");

const {
storeStorageLayoutForAllContracts,
Expand Down Expand Up @@ -879,3 +884,97 @@ subtask(
task("depositSSV").setAction(async (_, __, runSuper) => {
return runSuper();
});

// Defender
subtask(
"operateValidators",
"Creates the required amount of new SSV validators and stakes ETH"
)
.addOptionalParam("index", "Index of Native Staking contract", 1, types.int)
.addOptionalParam(
"stake",
"Stake 32 ether after registering a new SSV validator",
true,
types.boolean
)
.addOptionalParam(
"days",
"SSV Cluster operational time in days",
40,
types.int
)
.addOptionalParam("clear", "Clear storage", true, types.boolean)
.setAction(async (taskArgs) => {
const network = await ethers.provider.getNetwork();
const isMainnet = network.chainId === 1;
const isHolesky = network.chainId === 17000;
const addressesSet = isMainnet ? addresses.mainnet : addresses.holesky;

if (!isMainnet && !isHolesky) {
throw new Error(
"operate validatos is supported on Mainnet and Holesky only"
);
}

const storeFilePath = require("path").join(
__dirname,
"..",
`.localKeyValueStorage${isMainnet ? "Mainnet" : "Holesky"}`
);

const store = new KeyValueStoreClient({ path: storeFilePath });
const signer = await getDefenderSigner();

const WETH = await ethers.getContractAt("IWETH9", addressesSet.WETH);
const SSV = await ethers.getContractAt("IERC20", addressesSet.SSV);

// TODO: use index to target different native staking strategies when we have more than 1
const nativeStakingStrategy = await resolveContract(
"NativeStakingSSVStrategyProxy",
"NativeStakingSSVStrategy"
);

log(
"Balance of SSV tokens on the native staking contract: ",
formatUnits(await SSV.balanceOf(nativeStakingStrategy.address))
);

const contracts = {
nativeStakingStrategy,
WETH,
};
const feeAccumulatorAddress =
await nativeStakingStrategy.FEE_ACCUMULATOR_ADDRESS();

const p2p_api_key = isMainnet
? process.env.P2P_MAINNET_API_KEY
: process.env.P2P_HOLESKY_API_KEY;
if (!p2p_api_key) {
throw new Error(
"P2P API key environment variable is not set. P2P_MAINNET_API_KEY or P2P_HOLESKY_API_KEY"
);
}
const p2p_base_url = isMainnet ? "api.p2p.org" : "api-test-holesky.p2p.org";

const config = {
feeAccumulatorAddress,
p2p_api_key,
p2p_base_url,
// how much SSV (expressed in days of runway) gets deposited into the
// SSV Network contract on validator registration. This is calculated
// at a Cluster level rather than a single validator.
validatorSpawnOperationalPeriodInDays: taskArgs.days,
stake: taskArgs.stake,
clear: taskArgs.clear,
};

await operateValidators({
signer,
contracts,
store,
config,
});
});
task("operateValidators").setAction(async (_, __, runSuper) => {
return runSuper();
});
75 changes: 48 additions & 27 deletions contracts/tasks/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ const { v4: uuidv4 } = require("uuid");

const { resolveContract } = require("../utils/resolvers");
const { getSigner } = require("../utils/signers");
const { getClusterInfo } = require("../utils/ssv");
const { sleep } = require("../utils/time");
const { getClusterInfo } = require("./ssv");
const { logTxDetails } = require("../utils/txLogger");

const log = require("../utils/logger")("task:p2p");
Expand All @@ -31,15 +31,17 @@ const ERROR_THRESHOLD = 5;
* - if spawn process gets stuck at any of the above steps and is not able to
* recover in X amount of times (e.g. 5 times). Mark the process as failed
* and start over.
* - TODO: (implement this) if fuse of the native staking strategy is blown
* stop with all the operations
*/
const operateValidators = async ({ store, signer, contracts, config }) => {
const {
clear,
eigenPodAddress,
feeAccumulatorAddress,
p2p_api_key,
validatorSpawnOperationalPeriodInDays,
p2p_base_url,
validatorSpawnOperationalPeriodInDays,
stake,
clear,
} = config;

let currentState = await getState(store);
Expand All @@ -50,8 +52,13 @@ const operateValidators = async ({ store, signer, contracts, config }) => {
currentState = undefined;
}

if (!(await nodeDelegatorHas32Eth(contracts))) {
log(`Node delegator doesn't have enough ETH, exiting`);
if (!(await stakingContractHas32ETH(contracts))) {
log(`Native staking contract doesn't have enough ETH, exiting`);
return;
}

if (await stakingContractPaused(contracts)) {
log(`Native staking contract is paused... exiting`);
return;
}

Expand All @@ -61,8 +68,8 @@ const operateValidators = async ({ store, signer, contracts, config }) => {
await createValidatorRequest(
p2p_api_key, // api key
p2p_base_url,
contracts.nodeDelegator.address, // node delegator address
eigenPodAddress, // eigenPod address
contracts.nativeStakingStrategy.address, // SSV owner address & withdrawal address
feeAccumulatorAddress, // execution layer fee recipient
validatorSpawnOperationalPeriodInDays,
store
);
Expand All @@ -85,7 +92,7 @@ const operateValidators = async ({ store, signer, contracts, config }) => {
store,
currentState.uuid,
currentState.metadata,
contracts.nodeDelegator
contracts.nativeStakingStrategy
);
currentState = await getState(store);
}
Expand All @@ -94,7 +101,7 @@ const operateValidators = async ({ store, signer, contracts, config }) => {
await waitForTransactionAndUpdateStateOnSuccess(
store,
currentState.uuid,
contracts.nodeDelegator.provider,
contracts.nativeStakingStrategy.provider,
currentState.metadata.validatorRegistrationTx,
"registerSsvValidator", // name of transaction we are waiting for
"validator_registered" // new state when transaction confirmed
Expand All @@ -109,7 +116,7 @@ const operateValidators = async ({ store, signer, contracts, config }) => {
signer,
store,
currentState.uuid,
contracts.nodeDelegator,
contracts.nativeStakingStrategy,
currentState.metadata.depositData
);
currentState = await getState(store);
Expand All @@ -119,7 +126,7 @@ const operateValidators = async ({ store, signer, contracts, config }) => {
await waitForTransactionAndUpdateStateOnSuccess(
store,
currentState.uuid,
contracts.nodeDelegator.provider,
contracts.nativeStakingStrategy.provider,
currentState.metadata.depositTx,
"stakeEth", // name of transaction we are waiting for
"deposit_confirmed" // new state when transaction confirmed
Expand Down Expand Up @@ -249,14 +256,21 @@ const getState = async (store) => {
return JSON.parse(await store.get("currentRequest"));
};

const nodeDelegatorHas32Eth = async (contracts) => {
const address = contracts.nodeDelegator.address;
const stakingContractPaused = async (contracts) => {
const paused = await contracts.nativeStakingStrategy.paused();

log(`Native staking contract is ${paused ? "" : "not "}paused`);
return paused;
};

const stakingContractHas32ETH = async (contracts) => {
const address = contracts.nativeStakingStrategy.address;
const wethBalance = await contracts.WETH.balanceOf(address);
const ethBalance = await contracts.nodeDelegator.provider.getBalance(address);
const totalBalance = wethBalance.add(ethBalance);

log(`Node delegator has ${formatUnits(totalBalance, 18)} ETH in total`);
return totalBalance.gte(parseEther("32"));
log(
`Native staking contract has ${formatUnits(wethBalance, 18)} WETH in total`
);
return wethBalance.gte(parseEther("32"));
};

/* Make a GET or POST request to P2P service
Expand Down Expand Up @@ -302,8 +316,8 @@ const p2pRequest = async (url, api_key, method, body) => {
const createValidatorRequest = async (
p2p_api_key,
p2p_base_url,
nodeDelegatorAddress,
eigenPodAddress,
nativeStakingStrategy,
feeAccumulatorAddress,
validatorSpawnOperationalPeriodInDays,
store
) => {
Expand All @@ -315,9 +329,10 @@ const createValidatorRequest = async (
{
validatorsCount: 1,
id: uuid,
withdrawalAddress: eigenPodAddress,
feeRecipientAddress: nodeDelegatorAddress,
ssvOwnerAddress: nodeDelegatorAddress,
withdrawalAddress: nativeStakingStrategy,
feeRecipientAddress: feeAccumulatorAddress,
ssvOwnerAddress: nativeStakingStrategy,
// TODO: we need to alter this and store the key somewhere
type: "without-encrypt-key",
operationPeriodInDays: validatorSpawnOperationalPeriodInDays,
}
Expand Down Expand Up @@ -346,14 +361,20 @@ const waitForTransactionAndUpdateStateOnSuccess = async (
await updateState(uuid, newState, store);
};

const depositEth = async (signer, store, uuid, nodeDelegator, depositData) => {
const depositEth = async (
signer,
store,
uuid,
nativeStakingStrategy,
depositData
) => {
const { pubkey, signature, depositDataRoot } = depositData;
try {
log(`About to stake ETH with:`);
log(`pubkey: ${pubkey}`);
log(`signature: ${signature}`);
log(`depositDataRoot: ${depositDataRoot}`);
const tx = await nodeDelegator.connect(signer).stakeEth([
const tx = await nativeStakingStrategy.connect(signer).stakeEth([
{
pubkey,
signature,
Expand All @@ -378,7 +399,7 @@ const broadcastRegisterValidator = async (
store,
uuid,
metadata,
nodeDelegator
nativeStakingStrategy
) => {
const registerTransactionParams = defaultAbiCoder.decode(
[
Expand Down Expand Up @@ -412,7 +433,7 @@ const broadcastRegisterValidator = async (
log(`cluster: ${cluster}`);

try {
const tx = await nodeDelegator
const tx = await nativeStakingStrategy
.connect(signer)
.registerSsvValidator(
publicKey,
Expand Down
4 changes: 3 additions & 1 deletion contracts/utils/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ addresses.holesky.beaconChainDepositContract =
"0x4242424242424242424242424242424242424242";

addresses.holesky.OETHVaultProxy = "0x19d2bAaBA949eFfa163bFB9efB53ed8701aA5dD9";
// Address of the Holesky defender relayer
addresses.holesky.validatorRegistrator =
"0x1b94CA50D3Ad9f8368851F8526132272d1a5028C";
"0x3C6B0c7835a2E2E0A45889F64DcE4ee14c1D5CB4";

module.exports = addresses;
Loading