diff --git a/.gitignore b/.gitignore index 37f3a25c43..7770a7cb26 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,9 @@ contracts/coverage/ contracts/coverage.json contracts/build/ contracts/dist/ +contracts/.localKeyValueStorage +contracts/.localKeyValueStorageMainnet +contracts/.localKeyValueStorageHolesky todo.txt brownie/env-brownie/ diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 39651d573a..4d1550cf55 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -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, [] diff --git a/contracts/deploy/holesky/006_update_registrator.js b/contracts/deploy/holesky/006_update_registrator.js new file mode 100644 index 0000000000..c14bc8dcd3 --- /dev/null +++ b/contracts/deploy/holesky/006_update_registrator.js @@ -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; diff --git a/contracts/deployments/holesky/.migrations.json b/contracts/deployments/holesky/.migrations.json index 7e8f3cdbad..8448ae20e6 100644 --- a/contracts/deployments/holesky/.migrations.json +++ b/contracts/deployments/holesky/.migrations.json @@ -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 } \ No newline at end of file diff --git a/contracts/dev.env b/contracts/dev.env index 874bf5275d..f7754465b6 100644 --- a/contracts/dev.env +++ b/contracts/dev.env @@ -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] \ No newline at end of file diff --git a/contracts/package.json b/contracts/package.json index a45dee4888..973416ddf3 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -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", diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index d922fdf25f..55fd7c0b28 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -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, @@ -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(); +}); diff --git a/contracts/tasks/validator.js b/contracts/tasks/validator.js index 566dbef27c..9b9f484238 100644 --- a/contracts/tasks/validator.js +++ b/contracts/tasks/validator.js @@ -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"); @@ -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); @@ -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; } @@ -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 ); @@ -85,7 +92,7 @@ const operateValidators = async ({ store, signer, contracts, config }) => { store, currentState.uuid, currentState.metadata, - contracts.nodeDelegator + contracts.nativeStakingStrategy ); currentState = await getState(store); } @@ -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 @@ -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); @@ -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 @@ -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 @@ -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 ) => { @@ -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, } @@ -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, @@ -378,7 +399,7 @@ const broadcastRegisterValidator = async ( store, uuid, metadata, - nodeDelegator + nativeStakingStrategy ) => { const registerTransactionParams = defaultAbiCoder.decode( [ @@ -412,7 +433,7 @@ const broadcastRegisterValidator = async ( log(`cluster: ${cluster}`); try { - const tx = await nodeDelegator + const tx = await nativeStakingStrategy .connect(signer) .registerSsvValidator( publicKey, diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index aa6885e32e..4d2ac56496 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -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; diff --git a/contracts/utils/signers.js b/contracts/utils/signers.js index 3ec192d55c..439d6ee07f 100644 --- a/contracts/utils/signers.js +++ b/contracts/utils/signers.js @@ -66,16 +66,29 @@ const getDefenderSigner = async () => { ); process.exit(2); } + + const network = await ethers.provider.getNetwork(); + const isMainnet = network.chainId === 1; + const isHolesky = network.chainId === 17000; + + const apiKeyName = isMainnet + ? "DEFENDER_API_KEY" + : "HOLESKY_DEFENDER_API_KEY"; + const apiKeySecret = isMainnet + ? "DEFENDER_API_SECRET" + : "HOLESKY_DEFENDER_API_SECRET"; + const credentials = { - relayerApiKey: process.env.DEFENDER_API_KEY, - relayerApiSecret: process.env.DEFENDER_API_SECRET, + relayerApiKey: process.env[apiKeyName], + relayerApiSecret: process.env[apiKeySecret], }; + const client = new Defender(credentials); const provider = client.relaySigner.getProvider(); const signer = client.relaySigner.getSigner(provider, { speed }); log( - `Using Defender Relayer account ${await signer.getAddress()} from env vars DEFENDER_API_KEY and DEFENDER_API_SECRET` + `Using Defender Relayer account ${await signer.getAddress()} from env vars ${apiKeyName} and ${apiKeySecret}` ); return signer; }; @@ -112,4 +125,5 @@ module.exports = { getSigner, impersonateAccount, impersonateAndFund, + getDefenderSigner, }; diff --git a/contracts/utils/time.js b/contracts/utils/time.js new file mode 100644 index 0000000000..d761801638 --- /dev/null +++ b/contracts/utils/time.js @@ -0,0 +1,7 @@ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +module.exports = { + sleep, +}; diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 2ffb02816b..7b81d1aa61 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -1799,6 +1799,17 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +amazon-cognito-identity-js@^4.3.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-4.6.3.tgz#889410379a5fc5e883edc95f4ce233cc628e354c" + integrity sha512-MPVJfirbdmSGo7l4h7Kbn3ms1eJXT5Xq8ly+mCPPi8yAxaxdg7ouMUUNTqtDykoZxIdDLF/P6F3Zbg3dlGKOWg== + dependencies: + buffer "4.9.2" + crypto-js "^4.0.0" + fast-base64-decode "^1.0.0" + isomorphic-unfetch "^3.0.0" + js-cookie "^2.2.1" + amazon-cognito-identity-js@^6.0.1: version "6.2.0" resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.2.0.tgz#99e96666944429cb8f67b62e4cf7ad77fbe71ad0" @@ -2990,6 +3001,11 @@ crypto-browserify@3.12.0: randombytes "^2.0.0" randomfill "^1.0.3" +crypto-js@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + crypto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" @@ -3089,6 +3105,17 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +defender-base-client@1.38.1-rc.0: + version "1.38.1-rc.0" + resolved "https://registry.yarnpkg.com/defender-base-client/-/defender-base-client-1.38.1-rc.0.tgz#0d167845648a38bd2f5f80f5c50e081ec5f58b54" + integrity sha512-LNsuop5CE7fYyfWV3C4tlqkZawnxsMKW1SeF8TnobhZ6V5pO9LKyGSOo/V3KvPhO2/cTw5/S66+p1f6nkGCREA== + dependencies: + amazon-cognito-identity-js "^4.3.3" + async-retry "^1.3.3" + axios "^0.21.2" + lodash "^4.17.19" + node-fetch "^2.6.0" + defender-base-client@^1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/defender-base-client/-/defender-base-client-1.44.0.tgz#afe724447c0f9177b999b70b9f14dd70d61d5a7a" @@ -3100,6 +3127,17 @@ defender-base-client@^1.44.0: lodash "^4.17.19" node-fetch "^2.6.0" +defender-kvstore-client@^1.38.1-rc.0: + version "1.38.1-rc.0" + resolved "https://registry.yarnpkg.com/defender-kvstore-client/-/defender-kvstore-client-1.38.1-rc.0.tgz#6e33633a90e1355893141bad72298c6727abf30d" + integrity sha512-MeN8CtQSY72QFIys56Phxcn3tQ1nnmDXVtP/RlY9HCMynUO4zOQZaanDkgUmXTb+MYMj5VO3y+qfOfdEE+5EWA== + dependencies: + axios "^0.21.2" + defender-base-client "1.38.1-rc.0" + fs-extra "^10.0.0" + lodash "^4.17.19" + node-fetch "^2.6.0" + defer-to-connect@^1.0.1: version "1.1.3" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" @@ -6875,9 +6913,9 @@ promise@^8.0.0: dependencies: asap "~2.0.6" -"prompts@git+https://github.com/meshin-blox/prompts.git": +"prompts@https://github.com/meshin-blox/prompts.git": version "2.4.2" - resolved "git+https://github.com/meshin-blox/prompts.git#a22bdac044f6b32ba67adb4eacc2e58322512a2d" + resolved "https://github.com/meshin-blox/prompts.git#a22bdac044f6b32ba67adb4eacc2e58322512a2d" dependencies: kleur "^4.0.1" sisteransi "^1.0.5"