diff --git a/contracts/contracts/interfaces/ISSVNetwork.sol b/contracts/contracts/interfaces/ISSVNetwork.sol index 5ea6692152..1191575725 100644 --- a/contracts/contracts/interfaces/ISSVNetwork.sol +++ b/contracts/contracts/interfaces/ISSVNetwork.sol @@ -159,9 +159,8 @@ interface ISSVNetwork { function deposit( address clusterOwner, uint64[] memory operatorIds, - uint256 amount, Cluster memory cluster - ) external; + ) external payable; function executeOperatorFee(uint64 operatorId) external; @@ -201,11 +200,9 @@ interface ISSVNetwork { function proxiableUUID() external view returns (bytes32); - function reactivate( - uint64[] memory operatorIds, - uint256 amount, - Cluster memory cluster - ) external; + function reactivate(uint64[] memory operatorIds, Cluster memory cluster) + external + payable; function reduceOperatorFee(uint64 operatorId, uint256 fee) external; diff --git a/contracts/contracts/mocks/MockSSVNetwork.sol b/contracts/contracts/mocks/MockSSVNetwork.sol index 1b1c8d9db2..393c1aeaa2 100644 --- a/contracts/contracts/mocks/MockSSVNetwork.sol +++ b/contracts/contracts/mocks/MockSSVNetwork.sol @@ -32,9 +32,8 @@ contract MockSSVNetwork { function deposit( address clusterOwner, uint64[] calldata operatorIds, - uint256 amount, Cluster memory cluster - ) external {} + ) external payable {} function setFeeRecipientAddress(address recipient) external {} } diff --git a/contracts/deploy/mainnet/195_claim_ssv_from_staking_strategies.js b/contracts/deploy/mainnet/195_claim_ssv_from_staking_strategies.js new file mode 100644 index 0000000000..b5fab4e3d6 --- /dev/null +++ b/contracts/deploy/mainnet/195_claim_ssv_from_staking_strategies.js @@ -0,0 +1,77 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +const strategyConfigs = [ + { + proxyName: "NativeStakingSSVStrategy2Proxy", + label: "2nd Native Staking SSV Strategy", + }, + { + proxyName: "NativeStakingSSVStrategy3Proxy", + label: "3rd Native Staking SSV Strategy", + }, + { + proxyName: "CompoundingStakingSSVStrategyProxy", + label: "Compounding Staking SSV Strategy", + }, +]; + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "195_claim_ssv_from_staking_strategies", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ ethers }) => { + const ssv = await ethers.getContractAt("IERC20", addresses.mainnet.SSV); + const recipient = addresses.multichainStrategist; + const actions = []; + + let totalSSV = ethers.BigNumber.from(0); + + for (const strategyConfig of strategyConfigs) { + const proxy = await ethers.getContract(strategyConfig.proxyName); + const strategy = await ethers.getContractAt("IStrategy", proxy.address); + const balance = await ssv.balanceOf(strategy.address); + + console.log( + `${strategyConfig.label} ${ + strategy.address + } has ${ethers.utils.formatEther(balance)} SSV` + ); + + if (balance.isZero()) { + continue; + } + + actions.push({ + contract: strategy, + signature: "transferToken(address,uint256)", + args: [ssv.address, balance], + }); + + totalSSV = totalSSV.add(balance); + } + + if (totalSSV.isZero()) { + throw new Error("No SSV found on staking strategies"); + } + + actions.push({ + contract: ssv, + signature: "transfer(address,uint256)", + args: [recipient, totalSSV], + }); + + console.log( + `Transferring ${ethers.utils.formatEther(totalSSV)} SSV to ${recipient}` + ); + + return { + name: "Claim SSV from staking strategies and transfer to Guardian multisig", + actions, + }; + } +); diff --git a/contracts/deployments/mainnet/.migrations.json b/contracts/deployments/mainnet/.migrations.json index 0efc1a6fad..2aca2d2b4d 100644 --- a/contracts/deployments/mainnet/.migrations.json +++ b/contracts/deployments/mainnet/.migrations.json @@ -78,5 +78,6 @@ "189_harvesting_eip1271_ogn": 1774552993, "190_remove_3rd_native_staking_strategy": 1775468903, "192_migrate_ssv_clusters_to_eth": 1777859891, - "193_permissioned_rebase_module": 1778095857 + "193_permissioned_rebase_module": 1778095857, + "195_claim_ssv_from_staking_strategies": 1778480807 } diff --git a/contracts/tasks/ssv.js b/contracts/tasks/ssv.js index 448660b435..271cb394e5 100644 --- a/contracts/tasks/ssv.js +++ b/contracts/tasks/ssv.js @@ -60,10 +60,11 @@ const printClusterInfo = async (options) => { console.log(`Cluster: ${JSON.stringify(cluster.cluster, null, " ")}`); }; -const depositSSV = async ({ amount, index, operatorids }) => { +const depositCluster = async ({ amount, index, operatorids }) => { const amountBN = parseUnits(amount.toString(), 18); log(`Splitting operator IDs ${operatorids}`); - const operatorIds = await sortOperatorIds(operatorids); + const sortedOperatorIds = await sortOperatorIds(operatorids); + const operatorIds = splitOperatorIds(sortedOperatorIds); const signer = await getSigner(); @@ -78,22 +79,24 @@ const depositSSV = async ({ amount, index, operatorids }) => { const clusterInfo = await getClusterInfo({ chainId, ssvNetwork: ssvNetwork.address, - operatorids, + operatorids: sortedOperatorIds, ownerAddress: strategy.address, }); log( `About to deposit ${formatUnits( amountBN - )} SSV tokens to the SSV Network for native staking strategy ${ + )} ETH to the SSV Network for native staking strategy ${ strategy.address } with operator IDs ${operatorIds}` ); log(`Cluster: ${JSON.stringify(clusterInfo.cluster)}`); - const tx = await strategy + const tx = await ssvNetwork .connect(signer) - .depositSSV(operatorIds, amountBN, clusterInfo.cluster); - await logTxDetails(tx, "depositSSV"); + .deposit(strategy.address, operatorIds, clusterInfo.cluster, { + value: amountBN, + }); + await logTxDetails(tx, "depositCluster"); }; const migrateClusterToETH = async ({ type, amount, operatorids, index }) => { @@ -136,7 +139,7 @@ const migrateClusterToETH = async ({ type, amount, operatorids, index }) => { module.exports = { printClusterInfo, - depositSSV, + depositCluster, migrateClusterToETH, removeValidator, }; diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index 509f320323..af66480577 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -66,7 +66,7 @@ const { calculateMaxPricePerVoteTask, manageBribes } = require("./poolBooster"); const { updateVotemarketEpochsTask } = require("./votemarket"); const { manageMerklBribesTask } = require("./merklPoolBooster"); const { - depositSSV, + depositCluster, migrateClusterToETH, printClusterInfo, removeValidator: removeOldValidator, @@ -92,7 +92,6 @@ const { updateWOETHOraclePrice, } = require("./strategy"); const { - validatorOperationsConfig, exitValidator, doAccounting, manuallyFixAccounting, @@ -123,7 +122,6 @@ const { undelegateValidator, withdrawFromSFC, } = require("../utils/sonicActions"); -const { registerValidators, stakeValidators } = require("../utils/validator"); const { harvestAndSwap } = require("./harvest"); const { deployForceEtherSender, forceSend } = require("./simulation"); const { sleep } = require("../utils/time"); @@ -1160,10 +1158,10 @@ task("getClusterInfo").setAction(async (_, __, runSuper) => { }); subtask( - "depositSSV", - "Deposit SSV tokens from the native staking strategy into an SSV Cluster" + "depositCluster", + "Deposit ETH into an SSV cluster for a native staking strategy" ) - .addParam("amount", "Amount of SSV tokens to deposit", undefined, types.float) + .addParam("amount", "Amount of ETH to deposit", undefined, types.float) .addOptionalParam( "index", "The number of the Native Staking Contract deployed.", @@ -1176,8 +1174,8 @@ subtask( undefined, types.string ) - .setAction(depositSSV); -task("depositSSV").setAction(async (_, __, runSuper) => { + .setAction(depositCluster); +task("depositCluster").setAction(async (_, __, runSuper) => { return runSuper(); }); @@ -1273,77 +1271,6 @@ task("deployStakingProxy").setAction(async (_, __, runSuper) => { return runSuper(); }); -// Validator Operations - -subtask( - "registerValidators", - "Creates the required amount of new SSV validators and stakes ETH" -) - .addOptionalParam( - "days", - "SSV Cluster operational time in days", - 2, - types.int - ) - .addOptionalParam( - "validators", - "The number of validators to register. defaults to the max that can be registered", - undefined, - types.int - ) - .addOptionalParam("clear", "Clear storage", false, types.boolean) - .addOptionalParam( - "eth", - "Override the days option and set the amount of ETH to deposit to the cluster.", - undefined, - types.float - ) - .addOptionalParam( - "uuid", - "uuid of P2P's request SSV validator API call. Used to reprocess a registration that failed to get the SSV request status.", - undefined, - types.string - ) - .addOptionalParam( - "index", - "The number of the Native Staking Contract deployed.", - undefined, - types.int - ) - .setAction(async (taskArgs) => { - const config = await validatorOperationsConfig(taskArgs); - const signer = await getSigner(); - await registerValidators({ ...config, signer }); - }); -task("registerValidators").setAction(async (_, __, runSuper) => { - return runSuper(); -}); - -subtask( - "stakeValidators", - "Creates the required amount of new SSV validators and stakes ETH" -) - .addOptionalParam( - "uuid", - "uuid of P2P's request SSV validator API call", - undefined, - types.string - ) - .addOptionalParam( - "index", - "The number of the Native Staking Contract deployed.", - undefined, - types.int - ) - .setAction(async (taskArgs) => { - const config = await validatorOperationsConfig(taskArgs); - const signer = await getSigner(); - await stakeValidators({ ...config, signer }); - }); -task("stakeValidators").setAction(async (_, __, runSuper) => { - return runSuper(); -}); - /** * This function relays the messages between mainnet and base networks. * @@ -2434,10 +2361,10 @@ subtask( types.string ) .addOptionalParam( - "ssv", - "Amount of SSV to deposit to the cluster.", + "eth", + "Amount of ETH to deposit to the cluster.", 0, - types.int + types.float ) .setAction(async (taskArgs) => { await registerValidator(taskArgs); diff --git a/contracts/tasks/validator.js b/contracts/tasks/validator.js index bfdd24636c..9c3af39d73 100644 --- a/contracts/tasks/validator.js +++ b/contracts/tasks/validator.js @@ -13,90 +13,6 @@ const { sleep } = require("../utils/time"); const log = require("../utils/logger")("task:p2p"); -// This is in a separate file as it uses hardhat. -// We don't want the registerValidators and stakeValidators functions to use hardhat -// as they are using in Defender Actions. -// This is only used by Hardhat tasks registerValidators and stakeValidators -const validatorOperationsConfig = async (taskArgs) => { - const networkName = await getNetworkName(); - - const addressesSet = addresses[networkName]; - const isMainnet = networkName === "mainnet"; - - const WETH = await ethers.getContractAt("IWETH9", addressesSet.WETH); - - const nativeStakingStrategy = await resolveNativeStakingStrategyProxy( - taskArgs.index - ); - const feeAccumulatorAddress = - await nativeStakingStrategy.FEE_ACCUMULATOR_ADDRESS(); - - const p2p_api_key = isMainnet - ? process.env.P2P_MAINNET_API_KEY - : process.env.P2P_HOODI_API_KEY; - if (!p2p_api_key) { - throw new Error( - "P2P API key environment variable is not set. P2P_MAINNET_API_KEY or P2P_HOODI_API_KEY" - ); - } - const p2p_base_url = isMainnet ? "api.p2p.org" : "api-test.p2p.org"; - - const awsS3AccessKeyId = process.env.AWS_ACCESS_S3_KEY_ID; - const awsS3SexcretAccessKeyId = process.env.AWS_SECRET_S3_ACCESS_KEY; - const s3BucketName = process.env.VALIDATOR_KEYS_S3_BUCKET_NAME; - - if (!awsS3AccessKeyId) { - throw new Error("Secret AWS_ACCESS_S3_KEY_ID not set"); - } - if (!awsS3SexcretAccessKeyId) { - throw new Error("Secret AWS_SECRET_S3_ACCESS_KEY not set"); - } - if (!s3BucketName) { - throw new Error("Secret VALIDATOR_KEYS_S3_BUCKET_NAME not set"); - } - - // Convert the ETH amount to wei in string format if it is provided - const ethAmount = - taskArgs.eth >= 0 - ? parseEther(taskArgs.eth.toString()).toString() - : undefined; - - // Local/dev mode — simple in-memory store - const store = new Map(); - const kvClient = { - async get(key) { - return store.get(key) ?? null; - }, - async put(key, value) { - store.set(key, value); - return value; - }, - async del(key) { - store.delete(key); - }, - }; - - return { - store: kvClient, - p2p_api_key, - p2p_base_url, - nativeStakingStrategy, - feeAccumulatorAddress, - WETH, - // 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, - clear: taskArgs.clear, - uuid: taskArgs.uuid, - maxValidatorsToRegister: taskArgs.validators, - ethAmount, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName, - }; -}; - // @dev check validator is eligible for exit - // has been active for at least 256 epochs async function verifyMinActivationTime({ pubkey }) { @@ -403,7 +319,6 @@ const resolveFeeAccumulatorProxy = async (index) => { }; module.exports = { - validatorOperationsConfig, exitValidator, doAccounting, resetStakeETHTally, diff --git a/contracts/tasks/validatorCompound.js b/contracts/tasks/validatorCompound.js index 70cad05061..020ee136ed 100644 --- a/contracts/tasks/validatorCompound.js +++ b/contracts/tasks/validatorCompound.js @@ -86,7 +86,13 @@ async function registerValidatorCreateRequest({ days }) { * If the UUID is passed to this function then pubkey, shares, operatorIds are * ignored and fetched from the P2P */ -async function registerValidator({ pubkey, shares, operatorids, ssv, uuid }) { +async function registerValidator({ + pubkey, + shares, + operatorids, + eth = 0, + uuid, +}) { const signer = await getSigner(); if (uuid) { @@ -104,7 +110,7 @@ async function registerValidator({ pubkey, shares, operatorids, ssv, uuid }) { log(`Splitting operator IDs ${operatorids}`); const operatorIds = splitOperatorIds(operatorids); - const ssvAmount = parseUnits(ssv.toString(), 18); + const ethAmount = parseUnits(eth.toString(), 18); const strategy = await resolveContract( "CompoundingStakingSSVStrategyProxy", @@ -122,7 +128,9 @@ async function registerValidator({ pubkey, shares, operatorids, ssv, uuid }) { log(`About to register compounding validator with pubkey ${pubkey}`); const tx = await strategy .connect(signer) - .registerSsvValidator(pubkey, operatorIds, shares, ssvAmount, cluster); + .registerSsvValidator(pubkey, operatorIds, shares, cluster, { + value: ethAmount, + }); await logTxDetails(tx, "registerValidator"); } diff --git a/contracts/utils/validator.js b/contracts/utils/validator.js index 733d24dc1c..1d6c6ace5f 100644 --- a/contracts/utils/validator.js +++ b/contracts/utils/validator.js @@ -1,442 +1,6 @@ -const fetch = require("node-fetch"); -const { ethers } = require("ethers"); -const { defaultAbiCoder, formatUnits, hexDataSlice, parseEther, keccak256 } = - require("ethers").utils; -const { v4: uuidv4 } = require("uuid"); +const { formatUnits, parseEther } = require("ethers").utils; -const { storePrivateKeyToS3 } = require("./amazon"); -const { sleep } = require("./time"); -const { p2pApiEncodedKey } = require("./constants"); -const { mainnet } = require("./addresses"); -const { logTxDetails } = require("./txLogger"); - -const log = require("./logger")("task:p2p"); - -const validatorStateEnum = { - 0: "NOT_REGISTERED", - 1: "REGISTERED", - 2: "STAKED", - 3: "EXITED", - 4: "EXIT_COMPLETE", -}; - -/* When same UUID experiences and error threshold amount of times it is - * discarded. - */ -const ERROR_THRESHOLD = 5; -/* - * Spawns and maintains the required amount of validators throughout - * their setup cycle which consists of: - * - check balance of (W)ETH and crate P2P SSV cluster creation request - * - wait for the cluster to become operational - * - batch register the cluster on the SSV network - * - verify the complete cluster has been registered - * - batch stake the ETH to each of the validators - * - * Needs to also handle: - * - if anytime in the spawn cycle the number of (W)ETH falls below the - * required stake amount (withdrawal from Node Operator), mark the spawn - * process as failed - * - 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 registerValidators = async ({ - store, - signer, - p2p_api_key, - p2p_base_url, - nativeStakingStrategy, - feeAccumulatorAddress, - WETH, - validatorSpawnOperationalPeriodInDays, - clear, - uuid, - maxValidatorsToRegister, - ethAmount, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName, -}) => { - if (uuid && clear) { - throw new Error(`Can not clear state and use a uuid at the same time.`); - } - let currentState; - if (!uuid) { - // If starting a new registration or restarting a failed one - currentState = await getState(store); - log("currentState", currentState); - } else { - // If restarting a registration that failed to get the SSV request status - await clearState(uuid, store); - await updateState(uuid, "validator_creation_issued", store); - currentState = await getState(store); - log(`Processing uuid: ${uuid}`); - } - - // If clearing the local storage so a new registration can be started - if (clear && currentState?.uuid) { - await clearState(currentState.uuid, store); - currentState = undefined; - } - - // Calculate how many validators can be staked to - const validatorsForEth = await validatorsThatCanBeStaked( - nativeStakingStrategy, - WETH - ); - if (validatorsForEth == 0 || validatorsForEth < maxValidatorsToRegister) { - console.log( - `Native staking contract doesn't have enough WETH available to stake. Does depositToStrategy or resetStakeETHTally need to be called?` - ); - if (maxValidatorsToRegister) { - console.log( - `Requested to spawn ${maxValidatorsToRegister} validators but only ${validatorsForEth} can be spawned.` - ); - } - return; - } - const validatorsCount = - maxValidatorsToRegister === undefined || - validatorsForEth < maxValidatorsToRegister - ? validatorsForEth - : maxValidatorsToRegister; - log(`validatorsCount: ${validatorsCount}`); - - // Check if this Native Staking Contract is not paused - if (await stakingContractPaused(nativeStakingStrategy)) { - console.log(`Native staking contract is paused... exiting`); - return; - } - - const executeOperateLoop = async () => { - while (true) { - if (!currentState) { - await createValidatorRequest( - store, - "validator_creation_issued", // next state - p2p_api_key, - p2p_base_url, - nativeStakingStrategy.address, // SSV owner address & withdrawal address - feeAccumulatorAddress, // execution layer fee recipient - validatorSpawnOperationalPeriodInDays, - validatorsCount - ); - currentState = await getState(store); - } - - if (currentState.state === "validator_creation_issued") { - await confirmValidatorRegistered( - store, - currentState.uuid, - "validator_creation_confirmed", // next state - p2p_api_key, - p2p_base_url, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName - ); - currentState = await getState(store); - } - - if (currentState.state === "validator_creation_confirmed") { - await broadcastRegisterValidator( - store, - currentState.uuid, - "register_transaction_broadcast", // next state - signer, - currentState.metadata, - nativeStakingStrategy, - ethAmount - ); - currentState = await getState(store); - } - - if (currentState.state === "register_transaction_broadcast") { - await waitForTransactionAndUpdateStateOnSuccess( - store, - currentState.uuid, - "validator_registered", // next state - nativeStakingStrategy.provider, - currentState.metadata.validatorRegistrationTx, - "registerSsvValidator" // name of transaction we are waiting for - ); - currentState = await getState(store); - break; - } - - if (currentState.state === "validator_registered") { - log( - `Validator has been registered. Run the stakeValidators task to stake the validator` - ); - break; - } - - log(`Waiting for 5 seconds...`); - await sleep(5000); - } - }; - - try { - if ((await getErrorCount(store)) >= ERROR_THRESHOLD) { - await clearState( - currentState.uuid, - store, - `Errors have reached the threshold(${ERROR_THRESHOLD}) discarding attempt` - ); - return; - } - await executeOperateLoop(); - } catch (e) { - await increaseErrorCount(currentState ? currentState.uuid : "", store, e); - throw e; - } -}; - -const stakeValidators = async ({ - store, - signer, - nativeStakingStrategy, - WETH, - p2p_api_key, - p2p_base_url, - uuid, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName, -}) => { - if (await stakingContractPaused(nativeStakingStrategy)) { - log(`Native staking contract is paused... exiting`); - return; - } - - let currentState; - if (!uuid) { - currentState = await getState(store); - log("currentState", currentState); - - if (!currentState) { - log( - `There are no registered validators in local storage. Have you run registerValidators?` - ); - return; - } - } else { - log(`Processing uuid: ${uuid}`); - } - - const executeOperateLoop = async () => { - while (true) { - if (!currentState) { - await confirmValidatorRegistered( - store, - uuid, - "validator_registered", // next state - p2p_api_key, - p2p_base_url, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName - ); - currentState = await getState(store); - - // Check the first validator has not already been staked - const hashedPubkey = keccak256(currentState.metadata.pubkeys[0]); - const status = await nativeStakingStrategy.validatorsStates( - hashedPubkey - ); - if (validatorStateEnum[status] !== "REGISTERED") { - log( - `Validator with pubkey ${currentState.metadata.pubkeys[0]} not in REGISTERED state. Current state: ${validatorStateEnum[status]}` - ); - // await clearState(currentState.uuid, store); - // TODO just remove the validator that has already been staked from the metadata - break; - } else { - log( - `Validator with pubkey ${currentState.metadata.pubkeys[0]} is in the expected REGISTERED state.` - ); - } - } - - if (currentState.state === "validator_registered") { - await getDepositData( - store, - currentState.uuid, - "deposit_data_got", // next state - p2p_api_key, - p2p_base_url - ); - currentState = await getState(store); - } - - if (currentState.state === "deposit_data_got") { - const validatorsForEth = await validatorsThatCanBeStaked( - nativeStakingStrategy, - WETH - ); - const validatorsInState = currentState.metadata.pubkeys.length; - if (validatorsForEth < validatorsInState) { - `Native staking contract only has enough WETH to stake to ${validatorsForEth} validators, not ${validatorsInState}. Does depositToStrategy or resetStakeETHTally need to be called?`; - return; - } - - await depositEth( - store, - currentState.uuid, - "deposit_transaction_broadcast", // next state - signer, - nativeStakingStrategy, - currentState.metadata.pubkeys, - currentState.metadata.depositData - ); - currentState = await getState(store); - } - - if (currentState.state === "deposit_transaction_broadcast") { - await waitForTransactionAndUpdateStateOnSuccess( - store, - currentState.uuid, - "deposit_confirmed", // next state - nativeStakingStrategy.provider, - currentState.metadata.depositTx, - "stakeEth" // name of transaction we are waiting for - ); - - currentState = await getState(store); - } - - if (currentState.state === "deposit_confirmed") { - await clearState(currentState.uuid, store); - break; - } - - await sleep(1000); - } - }; - - try { - if ((await getErrorCount(store)) >= ERROR_THRESHOLD) { - await clearState( - currentState.uuid, - store, - `Errors have reached the threshold(${ERROR_THRESHOLD}) discarding attempt` - ); - return; - } - await executeOperateLoop(); - } catch (e) { - await increaseErrorCount(currentState ? currentState.uuid : "", store, e); - throw e; - } -}; - -const getErrorCount = async (store) => { - const existingRequest = await getState(store); - return existingRequest && existingRequest.errorCount - ? existingRequest.errorCount - : 0; -}; - -const increaseErrorCount = async (requestUUID, store, error) => { - if (!requestUUID) { - return; - } - - const existingRequest = await getState(store); - const existingErrorCount = existingRequest.errorCount - ? existingRequest.errorCount - : 0; - const newErrorCount = existingErrorCount + 1; - - await store.put( - "currentRequest", - JSON.stringify({ - ...existingRequest, - errorCount: newErrorCount, - }) - ); - log( - `Operate validators loop uuid: ${requestUUID} encountered an error ${newErrorCount} times. Error: `, - error - ); -}; - -/* Each P2P request has a life cycle that results in the following states stored - * in the shared Defender key-value storage memory. - * - "validator_creation_issued" the create request that creates a validator issued - * - "validator_creation_confirmed" confirmation that the validator has been created - * - "register_transaction_broadcast" the transaction to register the validator on - * the SSV network has been broadcast to the Ethereum network - * - "validator_registered" the register transaction has been confirmed - * - "deposit_transaction_broadcast" the stake transaction staking 32 ETH has been - * broadcast to the Ethereum network - * - "deposit_confirmed" transaction to stake 32 ETH has been confirmed - */ -const updateState = async (requestUUID, state, store, metadata = {}) => { - if ( - ![ - "validator_creation_issued", - "validator_creation_confirmed", - "register_transaction_broadcast", - "validator_registered", - "deposit_data_got", - "deposit_transaction_broadcast", - "deposit_confirmed", - ].includes(state) - ) { - throw new Error(`Unexpected state: ${state}`); - } - - const existingRequest = await getState(store); - const existingMetadata = - existingRequest && existingRequest.metadata ? existingRequest.metadata : {}; - - await store.put( - "currentRequest", - JSON.stringify({ - uuid: requestUUID, - state: state, - metadata: { ...existingMetadata, ...metadata }, - }) - ); -}; - -const clearState = async (uuid, store, error = false) => { - if (error) { - log( - `Clearing state tracking of ${uuid} request because of an error: ${error}` - ); - } else { - log( - `Clearing state tracking of ${uuid} request as it has completed its spawn cycle` - ); - } - await store.del("currentRequest"); -}; - -/* Fetches the state of the current/ongoing cluster creation if there is any - * returns either: - * - false if there is no cluster - * - - */ -const getState = async (store) => { - const currentState = await store.get("currentRequest"); - if (!currentState) { - return currentState; - } - - return JSON.parse(await store.get("currentRequest")); -}; - -const stakingContractPaused = async (nativeStakingStrategy) => { - const paused = await nativeStakingStrategy.paused(); - - log(`Native staking contract is ${paused ? "" : "not "}paused`); - return paused; -}; +const log = require("./logger")("utils:validator"); const validatorsThatCanBeStaked = async (nativeStakingStrategy, WETH) => { const address = nativeStakingStrategy.address; @@ -472,399 +36,6 @@ const validatorsThatCanBeStaked = async (nativeStakingStrategy, WETH) => { return validatorCount; }; -/* Make a GET or POST request to P2P API - * @param api_key: P2P API key - * @param method: http method that can either be POST or GET - * @param body: body object in case of a POST request - */ -const p2pRequest = async (url, api_key, method, body) => { - const headers = { - Accept: "application/json", - Authorization: `Bearer ${api_key}`, - }; - - if (method === "POST") { - headers["Content-Type"] = "application/json"; - } - - const bodyString = JSON.stringify(body); - log( - `About to call P2P API: ${method} ${url} `, - body != undefined ? ` and body: ${bodyString}` : "" - ); - - const rawResponse = await fetch(url, { - method, - headers, - body: bodyString, - }); - - const response = await rawResponse.json(); - if (response.error != null) { - log(`Call to P2P API failed: ${method} ${url}`); - // TODO: response might be too big for the logs to handle? - //log(`response: `, response); - throw new Error( - `Failed to call to P2P API. Error: ${JSON.stringify(response.error)}` - ); - } else { - log(`${method} request to P2P API succeeded:`); - // TODO: response might be too big for the logs to handle? - //log(response); - } - - return response; -}; - -const createValidatorRequest = async ( - store, - nextState, - p2p_api_key, - p2p_base_url, - nativeStakingStrategy, - feeAccumulatorAddress, - validatorSpawnOperationalPeriodInDays, - validatorsCount -) => { - const uuid = uuidv4(); - log(`About to create a SSV validator request with uuid: ${uuid}`); - await p2pRequest( - `https://${p2p_base_url}/api/v1/eth/staking/ssv/request/create`, - p2p_api_key, - "POST", - { - validatorsCount, - id: uuid, - withdrawalAddress: nativeStakingStrategy, - feeRecipientAddress: feeAccumulatorAddress, - ssvOwnerAddress: nativeStakingStrategy, - type: "with-encrypt-key", - operationPeriodInDays: validatorSpawnOperationalPeriodInDays, - ecdhPublicKey: p2pApiEncodedKey, - } - ); - - await updateState(uuid, nextState, store); - - log(`About to wait for 90 seconds for the P2P API to process the request...`); - await sleep(90000); -}; - -const waitForTransactionAndUpdateStateOnSuccess = async ( - store, - uuid, - nextState, - provider, - txHash, - methodName -) => { - log( - `Waiting for transaction with hash "${txHash}", method "${methodName}" and uuid "${uuid}" to be mined...` - ); - const tx = await provider.waitForTransaction(txHash); - if (!tx) { - throw Error( - `Transaction with hash "${txHash}" not found for method "${methodName}" and uuid "${uuid}"` - ); - } - if (tx.status !== 1) { - throw Error( - `Transaction with hash "${txHash}" failed for method "${methodName}" and uuid "${uuid}"` - ); - } - log( - `Transaction with hash "${txHash}", method "${methodName}" and uuid "${uuid}" has been mined` - ); - await updateState(uuid, nextState, store); -}; - -const depositEth = async ( - store, - uuid, - nextState, - signer, - nativeStakingStrategy, - pubkeys, - depositData -) => { - try { - log(`About to stake ETH with:`); - - // Check none of the validators are already registered - await depositFrontRunCheck(pubkeys, nativeStakingStrategy.provider); - - const validatorsStakeData = depositData.map((d) => ({ - pubkey: d.pubkey, - signature: d.signature, - depositDataRoot: d.depositDataRoot, - })); - log(`validators stake data: ${JSON.stringify(validatorsStakeData)}`); - const tx = await nativeStakingStrategy - .connect(signer) - .stakeEth(validatorsStakeData); - - log(`Transaction to stake ETH has been broadcast with hash: ${tx.hash}`); - - await updateState(uuid, nextState, store, { - depositTx: tx.hash, - }); - } catch (e) { - log(`Submitting transaction failed with: `, e); - //await clearState(uuid, store, `Transaction to deposit to validator fails`) - throw e; - } -}; - -const depositFrontRunCheck = async (pubkeys, provider) => { - const latestBlock = await provider.getBlockNumber(); - - // Create a contract instance - const depositContract = new ethers.Contract( - // Address - mainnet.beaconChainDepositContract, - // ABI - [ - "event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index)", - ], - provider - ); - - // Check the events from the last 1000 blocks - const recentBlocks = 1000; - const filter = { - address: depositContract.address, - topics: [ - "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5", - ], - fromBlock: latestBlock - recentBlocks, - toBlock: "latest", - }; - const logs = await provider.getLogs(filter); - log(`Checking ${logs.length} logs for duplicate deposits of public keys:`); - log(pubkeys); - - for (const eventLog of logs) { - const parsedLog = depositContract.interface.parseLog(eventLog); - const eventPubkey = parsedLog.args.pubkey; - - if (pubkeys.includes(eventPubkey.toLowerCase())) { - throw Error(`Validator with pubkey ${eventPubkey} has already deposited`); - } - } -}; - -const broadcastRegisterValidator = async ( - store, - uuid, - nextState, - signer, - metadata, - nativeStakingStrategy, - ethAmount -) => { - const registerTransactionParams = defaultAbiCoder.decode( - [ - "bytes", - "uint64[]", - "bytes", - "uint256", - "tuple(uint32, uint64, uint64, bool, uint256)", - ], - hexDataSlice(metadata.registerValidatorData, 4) - ); - // the publicKey and sharesData params are not encoded correctly by P2P so we will ignore them - const [, operatorIds, , amount, cluster] = registerTransactionParams; - // get publicKey and sharesData state storage - const publicKeys = metadata.pubkeys; - if (!publicKeys) { - throw Error(`pubkeys not found in metadata: ${metadata}`); - } - const { sharesData } = metadata; - if (!sharesData) { - throw Error(`sharesData not found in metadata: ${metadata}`); - } - - ethAmount = ethAmount !== undefined ? ethAmount : amount; - - // Check the first validator has not already been registered - const hashedPubkey = keccak256(metadata.pubkeys[0]); - const status = await nativeStakingStrategy.validatorsStates(hashedPubkey); - if (validatorStateEnum[status] !== "NOT_REGISTERED") { - log( - `Validator with pubkey ${metadata.pubkeys[0]} is not in NOT_REGISTERED state. Current state: ${validatorStateEnum[status]}` - ); - throw Error( - `public key has already been registered for uuid ${uuid}: ${metadata.pubkeys[0]} ` - ); - } - - log(`About to register validator with:`); - log(`publicKeys: ${publicKeys}`); - log(`operatorIds: ${operatorIds}`); - log(`sharesData: ${sharesData}`); - log(`ethAmount: ${ethAmount}`); - log(`cluster: ${cluster}`); - - try { - const tx = await nativeStakingStrategy - .connect(signer) - .registerSsvValidators(publicKeys, operatorIds, sharesData, cluster, { - value: ethAmount, - }); - - await logTxDetails(tx, "registerSsvValidators"); - - log( - `Transaction to register SSV Validator has been broadcast with hash: ${tx.hash}` - ); - - await updateState(uuid, nextState, store, { - validatorRegistrationTx: tx.hash, - }); - } catch (e) { - log(`Submitting transaction failed with: `, e); - //await clearState(uuid, store, `Transaction to register SSV Validator fails`) - throw e; - } -}; - -const confirmValidatorRegistered = async ( - store, - uuid, - nextState, - p2p_api_key, - p2p_base_url, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName -) => { - const doConfirmation = async () => { - if (!uuid) { - throw Error(`UUID is required to get validator status.`); - } - - const response = await p2pRequest( - `https://${p2p_base_url}/api/v1/eth/staking/ssv/request/status/${uuid}`, - p2p_api_key, - "GET" - ); - - const isReady = - response.result?.status === "ready" || - response.result?.status === "validator-ready"; - if (response.error != null) { - log( - `Error getting validator status with uuid ${uuid}: ${response.error}` - ); - log(response); - return false; - } else if (!isReady) { - log( - `Validators with request uuid ${uuid} are not ready yet. Status: ${response?.result?.status}` - ); - return false; - } else { - log(`Validators requested with uuid ${uuid} are ready`); - - const registerValidatorData = - response.result.validatorRegistrationTxs[0].data; - const sharesData = []; - const pubkeys = []; - const nonces = []; - const result = response.result; - for (let i = 0; i < result.encryptedShares.length; i++) { - const encryptedShare = result.encryptedShares[i]; - pubkeys[i] = encryptedShare.publicKey; - nonces[i] = encryptedShare.nonce; - sharesData[i] = encryptedShare.sharesData; - - await storePrivateKeyToS3({ - pubkey: pubkeys[i], - encryptedPrivateKey: encryptedShare.ecdhEncryptedPrivateKey, - awsS3AccessKeyId, - awsS3SexcretAccessKeyId, - s3BucketName, - }); - } - await updateState(uuid, nextState, store, { - pubkeys, - registerValidatorData, - sharesData, - }); - log(`Public keys: ${pubkeys}`); - log(`nonces: ${nonces}`); - log(`registerValidatorData: ${registerValidatorData}`); - return true; - } - }; - - await retry(doConfirmation, uuid, store); -}; - -const getDepositData = async ( - store, - uuid, - nextState, - p2p_api_key, - p2p_base_url -) => { - const doConfirmation = async () => { - if (!uuid) { - throw Error(`UUID is required to get deposit data.`); - } - const response = await p2pRequest( - `https://${p2p_base_url}/api/v1/eth/staking/ssv/request/deposit-data/${uuid}`, - p2p_api_key, - "GET" - ); - if (response.error != null) { - log(`Error getting deposit data with uuid ${uuid}: ${response.error}`); - // TODO: we shouldn't log full P2P responses. They break the logs - //log(response); - return false; - } else if (response.result?.status != "validator-ready") { - log( - `Deposit data with request uuid ${uuid} are not ready yet. Status: ${response.result?.status}` - ); - return false; - } else if (response.result?.status === "validator-ready") { - log(`Deposit data with request uuid ${uuid} is ready`); - - const depositData = response.result.depositData; - await updateState(uuid, nextState, store, { - depositData, - }); - log(`signature 0: ${depositData[0].signature}`); - log(`depositDataRoot 0: ${depositData[0].depositDataRoot}`); - return true; - } else { - log(`Error getting deposit data with uuid ${uuid}: ${response.error}`); - log(response); - throw Error(`Failed to get deposit data with uuid ${uuid}.`); - } - }; - - await retry(doConfirmation, uuid, store); -}; - -const retry = async (apiCall, uuid, store, attempts = 20) => { - let counter = 0; - while (true) { - if (await apiCall()) { - break; - } - counter++; - - if (counter > attempts) { - // Will not clear the state - throw new Error(`Failed P2P API call after ${attempts} attempts.`); - } - await sleep(3000); - } -}; - module.exports = { - registerValidators, - stakeValidators, validatorsThatCanBeStaked, };