diff --git a/bolt-cli/Cargo.lock b/bolt-cli/Cargo.lock index 19e873ca5..fd3d730ca 100644 --- a/bolt-cli/Cargo.lock +++ b/bolt-cli/Cargo.lock @@ -1283,7 +1283,7 @@ dependencies = [ [[package]] name = "bolt" -version = "0.1.2" +version = "0.1.3" dependencies = [ "alloy", "alloy-node-bindings", diff --git a/bolt-cli/Cargo.toml b/bolt-cli/Cargo.toml index d9451bcda..5013bf08d 100644 --- a/bolt-cli/Cargo.toml +++ b/bolt-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bolt" -version = "0.1.2" +version = "0.1.3" edition = "2021" description = "CLI for interacting with bolt protocol" authors = ["Chainbound Developers "] diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 4cafd2e28..39589e103 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -11,10 +11,7 @@ use clap::{ }; use reqwest::Url; -use crate::{ - common::{keystore::DEFAULT_KEYSTORE_PASSWORD, parse_ether_value}, - contracts::EigenLayerStrategy, -}; +use crate::common::{keystore::DEFAULT_KEYSTORE_PASSWORD, parse_ether_value}; /// `bolt` is a CLI tool to interact with bolt Protocol ✨ #[derive(Parser, Debug, Clone)] @@ -109,7 +106,7 @@ pub struct SendCommand { /// /// Leaving this empty will default to the canonical bolt RPC URL, which /// automatically manages increasing nonces on preconfirmed state. - #[clap(long, env = "BOLT_RPC_URL", default_value = "https://rpc-holesky.bolt.chainbound.io")] + #[clap(long, env = "BOLT_RPC_URL", default_value = "https://rpc.holesky.boltprotocol.xyz")] pub bolt_rpc_url: Url, /// The private key to sign the transaction with. @@ -216,12 +213,13 @@ pub struct OperatorsCommand { #[derive(Debug, Clone, Parser)] pub enum OperatorsSubcommand { /// Commands to interact with EigenLayer and bolt. - #[clap(name = "eigenlayer")] // and not eigen-layer + #[clap(name = "eigenlayer", alias = "el")] EigenLayer { #[clap(subcommand)] subcommand: EigenLayerSubcommand, }, /// Commands to interact with Symbiotic and bolt. + #[clap(alias = "symb")] Symbiotic { #[clap(subcommand)] subcommand: SymbioticSubcommand, @@ -240,9 +238,8 @@ pub enum EigenLayerSubcommand { operator_private_key: B256, /// The name of the strategy to deposit into. #[clap(long, env = "EIGENLAYER_STRATEGY")] - strategy: EigenLayerStrategy, - /// The amount to deposit into the strategy, in units (e.g. '1ether', '10gwei') - /// If no unit is provided, it is assumed to be in wei. + strategy: Address, + /// The amount to deposit into the strategy, in units (e.g. '1ether', '10gwei'). #[clap(long, env = "EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT", value_parser = parse_ether_value)] amount: U256, }, @@ -256,11 +253,17 @@ pub enum EigenLayerSubcommand { #[clap(long, env = "OPERATOR_PRIVATE_KEY")] operator_private_key: B256, /// The URL of the operator RPC. + /// If not provided, the "bolt RPC" URL is used, which is + /// consistent with bolt-sidecars running in "firewall delegation" mode. #[clap(long, env = "OPERATOR_RPC")] - operator_rpc: Url, - /// The salt for the operator signature. + operator_rpc: Option, + /// The operator's extra data string to be stored in the registry. + #[clap(long, env = "OPERATOR_EXTRA_DATA")] + extra_data: String, + /// The salt for the operator signature, to prevent replay attacks. + /// If not provided, a random value is used. #[clap(long, env = "OPERATOR_SIGNATURE_SALT")] - salt: B256, + salt: Option, }, /// Deregister an EigenLayer operator from the bolt AVS. @@ -288,13 +291,20 @@ pub enum EigenLayerSubcommand { /// Check the status of an operator in the bolt AVS. Status { - /// The URL of the RPC to broadcast the transaction. + /// The URL of the RPC to read data from #[clap(long, env = "RPC_URL")] rpc_url: Url, /// The address of the operator to check. #[clap(long, env = "OPERATOR_ADDRESS")] address: Address, }, + + /// List all whitelisted EigenLayer strategies + ListStrategies { + /// The URL of the RPC to read data from + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + }, } #[derive(Debug, Clone, Parser)] @@ -308,8 +318,13 @@ pub enum SymbioticSubcommand { #[clap(long, env = "OPERATOR_PRIVATE_KEY")] operator_private_key: B256, /// The URL of the operator RPC. + /// If not provided, the "bolt RPC" URL is used, which is + /// consistent with bolt-sidecars running in "firewall delegation" mode. #[clap(long, env = "OPERATOR_RPC")] - operator_rpc: Url, + operator_rpc: Option, + /// The operator's extra data string to be stored in the registry. + #[clap(long, env = "OPERATOR_EXTRA_DATA")] + extra_data: String, }, /// Deregister a Symbiotic operator from bolt. @@ -344,6 +359,13 @@ pub enum SymbioticSubcommand { #[clap(long, env = "OPERATOR_ADDRESS")] address: Address, }, + + /// List all whitelisted Symbiotic vaults + ListVaults { + /// The URL of the RPC to read data from + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + }, } #[derive(Debug, Clone, Parser)] @@ -513,6 +535,17 @@ impl Chain { } } + /// Get the bolt RPC URL for the given chain. + /// + /// Returns None if bolt RPC is not deployed for the chain. + pub fn bolt_rpc(&self) -> Option { + match self { + Self::Mainnet => Some(Url::parse("https://rpc.boltprotocol.xyz").unwrap()), + Self::Holesky => Some(Url::parse("https://rpc.holesky.boltprotocol.xyz").unwrap()), + _ => None, + } + } + /// Get the chain ID for the given chain. Returns `None` if the chain ID is not supported. pub fn from_id(id: u64) -> Option { match id { diff --git a/bolt-cli/src/commands/operators/eigenlayer.rs b/bolt-cli/src/commands/operators/eigenlayer.rs index 987238fa8..2a87f69d4 100644 --- a/bolt-cli/src/commands/operators/eigenlayer.rs +++ b/bolt-cli/src/commands/operators/eigenlayer.rs @@ -1,9 +1,7 @@ use alloy::{ + contract::Error as ContractError, network::EthereumWallet, - primitives::{ - utils::{format_ether, Unit}, - Bytes, Uint, U256, - }, + primitives::{utils::format_ether, Bytes, B256, U256}, providers::ProviderBuilder, signers::{local::PrivateKeySigner, SignerSync}, sol_types::SolInterface, @@ -11,26 +9,31 @@ use alloy::{ use chrono::{Duration, TimeDelta, Utc}; -use eyre::Context; +use eyre::{bail, Context}; use tracing::{info, warn}; use crate::{ cli::{Chain, EigenLayerSubcommand}, common::{ - bolt_manager::BoltManagerContract::{self, BoltManagerContractErrors}, - request_confirmation, try_parse_contract_error, + // bolt_manager::BoltManagerContract::{self, BoltManagerContractErrors}, + request_confirmation, + try_parse_contract_error, }, contracts::{ bolt::{ - BoltEigenLayerMiddleware::{self, BoltEigenLayerMiddlewareErrors}, + BoltEigenLayerMiddlewareHolesky::{self, BoltEigenLayerMiddlewareHoleskyErrors}, + BoltEigenLayerMiddlewareMainnet::{self, BoltEigenLayerMiddlewareMainnetErrors}, + BoltManager::{self, BoltManagerErrors}, + OperatorsRegistryV1::{self, OperatorsRegistryV1Errors}, SignatureWithSaltAndExpiry, }, deployments_for_chain, eigenlayer::{ - AVSDirectory, IStrategy::IStrategyInstance, IStrategyManager::IStrategyManagerInstance, + AVSDirectory, + IStrategy::{self, IStrategyInstance}, + IStrategyManager::IStrategyManagerInstance, }, - erc20::IERC20::IERC20Instance, - strategy_to_address, + erc20::IERC20::{self, IERC20Instance}, }, }; @@ -52,17 +55,13 @@ impl EigenLayerSubcommand { let deployments = deployments_for_chain(chain); - let strategy_address = - strategy_to_address(strategy, deployments.eigen_layer.supported_strategies); - let strategy_contract = IStrategyInstance::new(strategy_address, provider.clone()); - let strategy_manager_address = deployments.eigen_layer.strategy_manager; + let strategy_contract = IStrategyInstance::new(strategy, provider.clone()); + let strategy_manager_address = deployments.eigenlayer.strategy_manager; let strategy_manager = IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); let token = strategy_contract.underlyingToken().call().await?.token; - let amount = amount * Unit::ETHER.wei(); - info!(%strategy, %token, amount = format_ether(amount), ?operator, "Depositing funds into EigenLayer strategy"); request_confirmation(); @@ -71,6 +70,14 @@ impl EigenLayerSubcommand { let balance = token_erc20.balanceOf(operator).call().await?._0; + if amount > balance { + bail!( + "Insufficient balance: {} < {}", + format_ether(balance), + format_ether(amount) + ) + } + info!("Operator token balance: {}", format_ether(balance)); let result = token_erc20.approve(strategy_manager_address, amount).send().await?; @@ -79,10 +86,8 @@ impl EigenLayerSubcommand { let result = result.watch().await?; info!("Approval transaction included. Transaction hash: {:?}", result); - let result = strategy_manager - .depositIntoStrategy(strategy_address, token, amount) - .send() - .await?; + let result = + strategy_manager.depositIntoStrategy(strategy, token, amount).send().await?; info!(hash = ?result.tx_hash(), "Submitted deposit transaction, awaiting receipt..."); let receipt = result.get_receipt().await?; @@ -96,7 +101,7 @@ impl EigenLayerSubcommand { Ok(()) } - Self::Register { rpc_url, operator_rpc, salt, operator_private_key } => { + Self::Register { rpc_url, operator_rpc, salt, operator_private_key, extra_data } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; @@ -107,6 +112,10 @@ impl EigenLayerSubcommand { let chain = Chain::try_from_provider(&provider).await?; + let operator_rpc = operator_rpc.unwrap_or_else(|| chain.bolt_rpc().unwrap_or_else(|| + panic!("The bolt RPC is not deployed on {:?}. Please use the `--operator-rpc` flag to specify one manually.", chain)) + ); + info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering EigenLayer operator"); request_confirmation(); @@ -114,16 +123,15 @@ impl EigenLayerSubcommand { let deployments = deployments_for_chain(chain); let bolt_avs_address = deployments.bolt.eigenlayer_middleware; - let bolt_eigenlayer_middleware = - BoltEigenLayerMiddleware::new(bolt_avs_address, provider.clone()); - - let avs_directory = - AVSDirectory::new(deployments.eigen_layer.avs_directory, provider); const EXPIRY_DURATION: TimeDelta = Duration::minutes(20); let expiry = U256::from((Utc::now() + EXPIRY_DURATION).timestamp()); + let salt = salt.unwrap_or_else(|| B256::from_slice(&rand::random::<[u8; 32]>())); + + let avs_directory = + AVSDirectory::new(deployments.eigenlayer.avs_directory, &provider); - let signature_digest_hash = avs_directory + let signature_digest = avs_directory .calculateOperatorAVSRegistrationDigestHash( signer.address(), bolt_avs_address, @@ -134,44 +142,57 @@ impl EigenLayerSubcommand { .await? ._0; - let signature = - Bytes::from(signer.sign_hash_sync(&signature_digest_hash)?.as_bytes()); + let signature = Bytes::from(signer.sign_hash_sync(&signature_digest)?.as_bytes()); let signature = SignatureWithSaltAndExpiry { signature, expiry, salt }; - match bolt_eigenlayer_middleware - .registerOperator(operator_rpc.to_string(), signature) - .send() - .await - { - Ok(pending) => { - info!( - hash = ?pending.tx_hash(), - "registerOperator transaction sent, awaiting receipt..." - ); - - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) - } + // TODO(nico): consolidate holesky & mainnet smart contracts + if chain == Chain::Mainnet { + let el_middleware = + BoltEigenLayerMiddlewareMainnet::new(bolt_avs_address, provider.clone()); + + match el_middleware + .registerOperatorToAVS(operator_rpc.to_string(), extra_data, signature) + .send() + .await + { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } - info!("Succesfully registered EigenLayer operator"); + info!("Succesfully registered EigenLayer operator"); + } + Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } - Err(e) => { - match try_parse_contract_error::(e)? { - BoltEigenLayerMiddlewareErrors::AlreadyRegistered(_) => { - eyre::bail!("Operator already registered in bolt") - } - BoltEigenLayerMiddlewareErrors::NotOperator(_) => { - eyre::bail!("Operator not registered in EigenLayer") - } - BoltEigenLayerMiddlewareErrors::SaltSpent(_) => { - eyre::bail!("Salt already spent") + } else if chain == Chain::Holesky { + let el_middleware = + BoltEigenLayerMiddlewareHolesky::new(bolt_avs_address, provider.clone()); + + match el_middleware + .registerOperator(operator_rpc.to_string(), signature) + .send() + .await + { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) } - other => unreachable!( - "Unexpected error with selector {:?}", - other.selector() - ), + + info!("Succesfully registered EigenLayer operator"); } + Err(e) => parse_eigenlayer_middleware_holesky_errors(e)?, } } @@ -197,33 +218,46 @@ impl EigenLayerSubcommand { let deployments = deployments_for_chain(chain); let bolt_avs_address = deployments.bolt.eigenlayer_middleware; - let bolt_eigenlayer_middleware = - BoltEigenLayerMiddleware::new(bolt_avs_address, provider); - - match bolt_eigenlayer_middleware.deregisterOperator().send().await { - Ok(pending) => { - info!( - hash = ?pending.tx_hash(), - "deregisterOperator transaction sent, awaiting receipt..." - ); - - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) - } - info!("Succesfully deregistered EigenLayer operator"); + if chain == Chain::Mainnet { + let el_middleware = + BoltEigenLayerMiddlewareMainnet::new(bolt_avs_address, provider); + + match el_middleware.deregisterOperatorFromAVS().send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "deregisterOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully deregistered EigenLayer operator"); + } + Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } - Err(e) => { - match try_parse_contract_error::(e)? { - BoltEigenLayerMiddlewareErrors::NotRegistered(_) => { - eyre::bail!("Operator not registered in bolt") + } else if chain == Chain::Holesky { + let el_middleware = + BoltEigenLayerMiddlewareHolesky::new(bolt_avs_address, provider); + + match el_middleware.deregisterOperator().send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "deregisterOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) } - other => unreachable!( - "Unexpected error with selector {:?}", - other.selector() - ), + + info!("Succesfully deregistered EigenLayer operator"); } + Err(e) => parse_eigenlayer_middleware_holesky_errors(e)?, } } @@ -248,38 +282,71 @@ impl EigenLayerSubcommand { let deployments = deployments_for_chain(chain); - let bolt_manager = - BoltManagerContract::new(deployments.bolt.manager, provider.clone()); - if bolt_manager.isOperator(address).call().await?._0 { - info!(?address, "EigenLayer operator is registered"); - } else { - warn!(?address, "Operator not registered"); - return Ok(()); - } - - match bolt_manager.updateOperatorRPC(operator_rpc.to_string()).send().await { - Ok(pending) => { - info!( - hash = ?pending.tx_hash(), - "updateOperatorRPC transaction sent, awaiting receipt..." - ); + // TODO(nico): consolidate holesky & mainnet smart contracts + if chain == Chain::Mainnet { + let el_middleware = BoltEigenLayerMiddlewareMainnet::new( + deployments.bolt.eigenlayer_middleware, + provider.clone(), + ); + + match el_middleware + .updateOperatorRpcEndpoint(operator_rpc.to_string()) + .send() + .await + { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "updateOperatorRPCEndpoint transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + info!("Succesfully updated EigenLayer operator RPC"); } - - info!("Succesfully updated EigenLayer operator RPC"); + Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } - Err(e) => match try_parse_contract_error::(e)? { - BoltManagerContractErrors::OperatorNotRegistered(_) => { - eyre::bail!("Operator not registered in bolt") - } - other => { - unreachable!("Unexpected error with selector {:?}", other.selector()) + } else if chain == Chain::Holesky { + let bolt_manager = BoltManager::new(deployments.bolt.manager, provider.clone()); + + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "EigenLayer operator is registered"); + } else { + warn!(?address, "Operator not registered"); + return Ok(()); + } + + match bolt_manager.updateOperatorRPC(operator_rpc.to_string()).send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "updateOperatorRPC transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully updated EigenLayer operator RPC"); } - }, + Err(e) => match try_parse_contract_error::(e)? { + BoltManagerErrors::OperatorNotRegistered(_) => { + eyre::bail!("Operator not registered in bolt") + } + other => { + unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ) + } + }, + } } + Ok(()) } @@ -289,57 +356,158 @@ impl EigenLayerSubcommand { let chain = Chain::try_from_provider(&provider).await?; let deployments = deployments_for_chain(chain); - let bolt_manager = - BoltManagerContract::new(deployments.bolt.manager, provider.clone()); - if bolt_manager.isOperator(address).call().await?._0 { - info!(?address, "EigenLayer operator is registered"); - } else { - warn!(?address, "Operator not registered"); - return Ok(()) - } - match bolt_manager.getOperatorData(address).call().await { - Ok(operator_data) => { - info!(?address, operator_data = ?operator_data._0, "Operator data"); + info!(?address, ?chain, "Checking EigenLayer operator status"); + + if chain == Chain::Mainnet { + let el_middleware = BoltEigenLayerMiddlewareMainnet::new( + deployments.bolt.eigenlayer_middleware, + provider.clone(), + ); + + let registry = OperatorsRegistryV1::new( + deployments.bolt.operators_registry, + provider.clone(), + ); + + // TOOD: clean up, concurrent calls + match registry.isOperator(address).call().await { + Ok(is_operator) => { + if is_operator._0 { + info!(?address, "EigenLayer operator is registered"); + } else { + warn!(?address, "Operator not registered"); + return Ok(()) + } + } + Err(e) => { + let other = try_parse_contract_error::(e)?; + bail!("Unexpected error with selector {:?}", other.selector()) + } } - Err(e) => match try_parse_contract_error::(e)? { - BoltManagerContractErrors::KeyNotFound(_) => { - warn!(?address, "Operator data not found"); + + match registry.isActiveOperator(address).call().await { + Ok(is_active) => { + if is_active._0 { + info!(?address, "Operator is active"); + } else { + warn!(?address, "Operator is not active yet"); + } } - other => { - unreachable!("Unexpected error with selector {:?}", other.selector()) + Err(e) => { + let other = try_parse_contract_error::(e)?; + bail!("Unexpected error with selector {:?}", other.selector()) } - }, - } + } + + match el_middleware.getOperatorCollaterals(address).call().await { + Ok(collaterals) => { + for (token, amount) in collaterals._0.iter().zip(collaterals._1.iter()) + { + if !amount.is_zero() { + info!(?address, token = %token, amount = format_ether(*amount), "Operator has collateral"); + } + } + + let total_collateral = collaterals._1.iter().sum::(); + info!( + ?address, + "Total operator collateral: {}", + format_ether(total_collateral) + ); + } + Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, + } + } else if chain == Chain::Holesky { + let bolt_manager = BoltManager::new(deployments.bolt.manager, provider.clone()); + + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "EigenLayer operator is registered"); + } else { + warn!(?address, "Operator not registered"); + return Ok(()) + } + + let middleware = BoltEigenLayerMiddlewareHolesky::new( + deployments.bolt.eigenlayer_middleware, + provider.clone(), + ); + + match bolt_manager.getOperatorData(address).call().await { + Ok(operator_data) => { + info!(?address, operator_data = ?operator_data._0, "Operator data"); + } + Err(e) => match try_parse_contract_error::(e)? { + BoltManagerErrors::KeyNotFound(key) => { + warn!(?address, "Operator data not found: {:?}", key.key); + } + other => { + unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ) + } + }, + } - // Check if operator has collateral - let mut total_collateral = Uint::from(0); - for (name, collateral) in deployments.collateral { - let stake = - match bolt_manager.getOperatorStake(address, collateral).call().await { - Ok(stake) => stake._0, - Err(e) => { - match try_parse_contract_error::(e)? - { - BoltEigenLayerMiddlewareErrors::KeyNotFound(_) => Uint::from(0), - other => unreachable!( - "Unexpected error with selector {:?}", - other.selector() - ), + match middleware.getOperatorCollaterals(address).call().await { + Ok(collaterals) => { + for (token, amount) in collaterals._0.iter().zip(collaterals._1.iter()) + { + if !amount.is_zero() { + info!(?address, token = %token, amount = format_ether(*amount), "Operator has collateral"); } } - }; - if stake > Uint::from(0) { - total_collateral += stake; - info!(?address, token = %name, amount = ?stake, "Operator has collateral"); + + let total_collateral = collaterals._1.iter().sum::(); + info!( + ?address, + "Total operator collateral: {}", + format_ether(total_collateral) + ); + } + Err(e) => parse_eigenlayer_middleware_mainnet_errors(e)?, } } - if total_collateral >= Unit::ETHER.wei() { - info!(?address, total_collateral=?total_collateral, "Operator is active"); - } else if total_collateral > Uint::from(0) { - info!(?address, total_collateral=?total_collateral, "Total operator collateral"); + + Ok(()) + } + + Self::ListStrategies { rpc_url } => { + let provider = ProviderBuilder::new().on_http(rpc_url.clone()); + + let chain = Chain::try_from_provider(&provider).await?; + + let deployments = deployments_for_chain(chain); + + info!("Listing all EigenLayer whitelisted strategies:"); + + // TODO(nico): consolidate holesky & mainnet smart contracts + let strategies = if chain == Chain::Mainnet { + let el_middleware = BoltEigenLayerMiddlewareMainnet::new( + deployments.bolt.eigenlayer_middleware, + &provider, + ); + + el_middleware.getActiveWhitelistedStrategies().call().await?._0 + } else if chain == Chain::Holesky { + let el_middleware = BoltEigenLayerMiddlewareHolesky::new( + deployments.bolt.eigenlayer_middleware, + &provider, + ); + + el_middleware.getRestakeableStrategies().call().await?._0 } else { - warn!(?address, "Operator has no collateral"); + unreachable!("Invalid chain"); + }; + + for strategy_address in strategies { + let strategy = IStrategy::new(strategy_address, &provider); + let token_address = strategy.underlyingToken().call().await?.token; + let token = IERC20::new(token_address, &provider); + let token_symbol = token.symbol().call().await?._0; + + info!("- Token: {} - Strategy: {}", token_symbol, strategy_address); } Ok(()) @@ -348,6 +516,47 @@ impl EigenLayerSubcommand { } } +/// Parse EigenLayer middleware errors. +fn parse_eigenlayer_middleware_holesky_errors(err: ContractError) -> eyre::Result<()> { + match try_parse_contract_error::(err)? { + BoltEigenLayerMiddlewareHoleskyErrors::AlreadyRegistered(_) => { + bail!("Operator already registered in bolt") + } + BoltEigenLayerMiddlewareHoleskyErrors::NotOperator(_) => { + bail!("Operator not registered in EigenLayer") + } + BoltEigenLayerMiddlewareHoleskyErrors::SaltSpent(_) => { + bail!("Salt already spent") + } + BoltEigenLayerMiddlewareHoleskyErrors::NotRegistered(_) => { + bail!("Operator not registered in bolt") + } + BoltEigenLayerMiddlewareHoleskyErrors::KeyNotFound(_) => bail!("Key not found"), + BoltEigenLayerMiddlewareHoleskyErrors::NotActivelyDelegated(_) => { + bail!("Operator not actively delegated") + } + BoltEigenLayerMiddlewareHoleskyErrors::OperatorNotRegistered(_) => { + bail!("Operator not registered in bolt") + } + } +} + +/// Parse EigenLayer middleware errors. +fn parse_eigenlayer_middleware_mainnet_errors(err: ContractError) -> eyre::Result<()> { + match try_parse_contract_error::(err)? { + BoltEigenLayerMiddlewareMainnetErrors::InvalidRpc(_) => bail!("Invalid RPC URL"), + BoltEigenLayerMiddlewareMainnetErrors::InvalidMiddleware(inner) => { + bail!("Invalid middleware: {}", inner.reason) + } + BoltEigenLayerMiddlewareMainnetErrors::InvalidSigner(_) => bail!("Invalid signer"), + BoltEigenLayerMiddlewareMainnetErrors::OnlyRestakingMiddlewares(_) => { + bail!("Only restaking middlewares are allowed to call this function") + } + BoltEigenLayerMiddlewareMainnetErrors::Unauthorized(_) => bail!("Unauthorized call"), + BoltEigenLayerMiddlewareMainnetErrors::UnknownOperator(_) => bail!("Unknown operator"), + } +} + #[cfg(test)] mod tests { use crate::{ @@ -355,13 +564,13 @@ mod tests { contracts::{ deployments_for_chain, eigenlayer::{DelegationManager, IStrategy}, - strategy_to_address, EigenLayerStrategy, }, }; + use alloy::{ network::EthereumWallet, node_bindings::Anvil, - primitives::{keccak256, utils::parse_units, Address, B256, U256}, + primitives::{address, keccak256, utils::parse_units, Address, U256}, providers::{ext::AnvilApi, Provider, ProviderBuilder, WalletProvider}, signers::local::PrivateKeySigner, sol_types::SolValue, @@ -369,7 +578,7 @@ mod tests { use alloy_node_bindings::WEI_IN_ETHER; #[tokio::test] - async fn test_eigenlayer_flow() { + async fn test_eigenlayer_flow_holesky() { let _ = tracing_subscriber::fmt::try_init(); let s1 = PrivateKeySigner::random(); let secret_key = s1.to_bytes(); @@ -394,10 +603,7 @@ mod tests { let deployments = deployments_for_chain(Chain::Holesky); - let weth_strategy_address = strategy_to_address( - EigenLayerStrategy::WEth, - deployments.eigen_layer.supported_strategies, - ); + let weth_strategy_address = address!("80528D6e9A2BAbFc766965E0E26d5aB08D9CFaF9"); let strategy = IStrategy::new(weth_strategy_address, provider.clone()); let weth_address = strategy.underlyingToken().call().await.expect("underlying token").token; @@ -413,7 +619,7 @@ mod tests { // EigenLayer CLI, but we do it here for testing purposes. let delegation_manager = - DelegationManager::new(deployments.eigen_layer.delegation_manager, provider.clone()); + DelegationManager::new(deployments.eigenlayer.delegation_manager, provider.clone()); let receipt = delegation_manager .registerAsOperator(Address::ZERO, 0, "https://bolt.chainbound.io/rpc".to_string()) @@ -442,7 +648,7 @@ mod tests { subcommand: EigenLayerSubcommand::Deposit { rpc_url: anvil_url.clone(), operator_private_key: secret_key, - strategy: EigenLayerStrategy::WEth, + strategy: weth_strategy_address, amount: U256::from(1), }, }, @@ -457,8 +663,9 @@ mod tests { subcommand: EigenLayerSubcommand::Register { rpc_url: anvil_url.clone(), operator_private_key: secret_key, - operator_rpc: "https://bolt.chainbound.io/rpc".parse().expect("valid url"), - salt: B256::ZERO, + extra_data: "hello world computer 🌐".to_string(), + operator_rpc: None, + salt: None, }, }, }; @@ -519,4 +726,10 @@ mod tests { check_operator_registration.run().await.expect("to check operator registration"); } + + #[tokio::test] + async fn test_eigenlayer_flow_mainnet() { + // TODO: do the same as above, but fork from mainnet instead + // and use the mainnet EigenLayer contracts + } } diff --git a/bolt-cli/src/commands/operators/symbiotic.rs b/bolt-cli/src/commands/operators/symbiotic.rs index 222cfce43..076273f39 100644 --- a/bolt-cli/src/commands/operators/symbiotic.rs +++ b/bolt-cli/src/commands/operators/symbiotic.rs @@ -1,23 +1,29 @@ use alloy::{ + contract::Error as ContractError, network::EthereumWallet, - primitives::{utils::Unit, Uint}, + primitives::{ + utils::format_ether, + U256, + }, providers::ProviderBuilder, signers::local::PrivateKeySigner, sol_types::SolInterface, }; -use eyre::Context; +use eyre::{bail, Context}; use tracing::{info, warn}; use crate::{ cli::{Chain, SymbioticSubcommand}, common::{ - bolt_manager::BoltManagerContract::{self, BoltManagerContractErrors}, request_confirmation, try_parse_contract_error, }, contracts::{ - bolt::BoltSymbioticMiddleware::{self, BoltSymbioticMiddlewareErrors}, - deployments_for_chain, - symbiotic::IOptInService, + bolt::{ + BoltManager::{self, BoltManagerErrors}, + BoltSymbioticMiddlewareHolesky::{self, BoltSymbioticMiddlewareHoleskyErrors}, + BoltSymbioticMiddlewareMainnet::{self, BoltSymbioticMiddlewareMainnetErrors}, + OperatorsRegistryV1::{self, OperatorsRegistryV1Errors} + }, deployments_for_chain, erc20::IERC20, symbiotic::{IOptInService, IVault} }, }; @@ -25,7 +31,7 @@ impl SymbioticSubcommand { /// Run the symbiotic subcommand. pub async fn run(self) -> eyre::Result<()> { match self { - Self::Register { operator_rpc, operator_private_key, rpc_url } => { + Self::Register { operator_rpc, operator_private_key, rpc_url, extra_data } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; @@ -38,6 +44,10 @@ impl SymbioticSubcommand { let deployments = deployments_for_chain(chain); + let operator_rpc = operator_rpc.unwrap_or_else(|| chain.bolt_rpc().unwrap_or_else(|| + panic!("The bolt RPC is not deployed on {:?}. Please use the `--operator-rpc` flag to specify one manually.", chain)) + ); + info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering Symbiotic operator"); request_confirmation(); @@ -59,36 +69,54 @@ impl SymbioticSubcommand { ); } - let middleware = BoltSymbioticMiddleware::new( - deployments.bolt.symbiotic_middleware, - provider.clone(), - ); + if chain == Chain::Mainnet { + let middleware = BoltSymbioticMiddlewareMainnet::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); - match middleware.registerOperator(operator_rpc.to_string()).send().await { - Ok(pending) => { - info!( - hash = ?pending.tx_hash(), - "registerOperator transaction sent, awaiting receipt..." - ); + match middleware + .registerOperator(operator_rpc.to_string(), extra_data) + .send() + .await + { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) + info!("Succesfully registered Symbiotic operator"); } - - info!("Succesfully registered Symbiotic operator"); + Err(e) => parse_symbiotic_middleware_mainnet_errors(e)?, } - Err(e) => match try_parse_contract_error::(e)? { - BoltSymbioticMiddlewareErrors::AlreadyRegistered(_) => { - eyre::bail!("Operator already registered in bolt") - } - BoltSymbioticMiddlewareErrors::NotOperator(_) => { - eyre::bail!("Operator not registered in Symbiotic") - } - other => { - unreachable!("Unexpected error with selector {:?}", other.selector()) + } else if chain == Chain::Holesky { + let middleware = BoltSymbioticMiddlewareHolesky::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); + + match middleware.registerOperator(operator_rpc.to_string()).send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered Symbiotic operator"); } - }, + Err(e) => parse_symbiotic_middleware_holesky_errors(e)?, + } } Ok(()) @@ -113,31 +141,51 @@ impl SymbioticSubcommand { request_confirmation(); - let middleware = - BoltSymbioticMiddleware::new(deployments.bolt.symbiotic_middleware, provider); + // TODO(nico): consolidate holesky & mainnet smart contracts + if chain == Chain::Mainnet { + let middleware = BoltSymbioticMiddlewareMainnet::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); - match middleware.deregisterOperator().send().await { - Ok(pending) => { - info!( - hash = ?pending.tx_hash(), - "deregisterOperator transaction sent, awaiting receipt..." - ); + match middleware.deregisterOperator().send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "deregisterOperator transaction sent, awaiting receipt..." + ); - let receipt = pending.get_receipt().await?; - if !receipt.status() { - eyre::bail!("Transaction failed: {:?}", receipt) - } + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } - info!("Succesfully deregistered Symbiotic operator"); - } - Err(e) => match try_parse_contract_error::(e)? { - BoltSymbioticMiddlewareErrors::NotRegistered(_) => { - eyre::bail!("Operator not registered in bolt") + info!("Succesfully deregistered Symbiotic operator"); } - other => { - unreachable!("Unexpected error with selector {:?}", other.selector()) + Err(e) => parse_symbiotic_middleware_mainnet_errors(e)?, + } + } else if chain == Chain::Holesky { + let middleware = BoltSymbioticMiddlewareHolesky::new( + deployments.bolt.symbiotic_middleware, + provider, + ); + + match middleware.deregisterOperator().send().await { + Ok(pending) => { + info!( + hash = ?pending.tx_hash(), + "deregisterOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully deregistered Symbiotic operator"); } - }, + Err(e) => parse_symbiotic_middleware_holesky_errors(e)?, + } } Ok(()) @@ -161,8 +209,7 @@ impl SymbioticSubcommand { let deployments = deployments_for_chain(chain); - let bolt_manager = - BoltManagerContract::new(deployments.bolt.manager, provider.clone()); + let bolt_manager = BoltManager::new(deployments.bolt.manager, provider.clone()); if bolt_manager.isOperator(address).call().await?._0 { info!(?address, "Symbiotic operator is registered"); } else { @@ -184,8 +231,8 @@ impl SymbioticSubcommand { info!("Succesfully updated Symbiotic operator RPC"); } - Err(e) => match try_parse_contract_error::(e)? { - BoltManagerContractErrors::OperatorNotRegistered(_) => { + Err(e) => match try_parse_contract_error::(e)? { + BoltManagerErrors::OperatorNotRegistered(_) => { eyre::bail!("Operator not registered in bolt") } other => { @@ -202,57 +249,156 @@ impl SymbioticSubcommand { let chain = Chain::try_from_provider(&provider).await?; let deployments = deployments_for_chain(chain); - let bolt_manager = - BoltManagerContract::new(deployments.bolt.manager, provider.clone()); - if bolt_manager.isOperator(address).call().await?._0 { - info!(?address, "Symbiotic operator is registered"); - } else { - warn!(?address, "Operator not registered"); - return Ok(()) - } - match bolt_manager.getOperatorData(address).call().await { - Ok(operator_data) => { - info!(?address, operator_data = ?operator_data._0, "Operator data"); + info!(?address, ?chain, "Checking Symbiotic operator status"); + + // TODO(nico): consolidate holesky & mainnet smart contracts + if chain == Chain::Mainnet { + let middleware = BoltSymbioticMiddlewareMainnet::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); + + let registry = OperatorsRegistryV1::new( + deployments.bolt.operators_registry, + provider.clone(), + ); + + // TOOD: clean up, concurrent calls + match registry.isOperator(address).call().await { + Ok(is_operator) => { + if is_operator._0 { + info!(?address, "Symbiotic operator is registered"); + } else { + warn!(?address, "Operator not registered"); + return Ok(()) + } + } + Err(e) => { + let other = try_parse_contract_error::(e)?; + bail!("Unexpected error with selector {:?}", other.selector()) + }, } - Err(e) => match try_parse_contract_error::(e)? { - BoltManagerContractErrors::KeyNotFound(_) => { - warn!(?address, "Operator data not found"); + + match registry.isActiveOperator(address).call().await { + Ok(is_active) => { + if is_active._0 { + info!(?address, "Operator is active"); + } else { + warn!(?address, "Operator is not active yet"); + } } - other => { - unreachable!("Unexpected error with selector {:?}", other.selector()) + Err(e) => { + let other = try_parse_contract_error::(e)?; + bail!("Unexpected error with selector {:?}", other.selector()) + }, + } + + match middleware.getOperatorCollaterals(address).call().await { + Ok(collaterals) => { + for (token, amount) in collaterals._0.iter().zip(collaterals._1.iter()) + { + if !amount.is_zero() { + info!(?address, token = %token, amount = format_ether(*amount), "Operator has collateral"); + } + } + + let total_collateral = collaterals._1.iter().sum::(); + info!( + ?address, + "Total operator collateral: {}", + format_ether(total_collateral) + ); } - }, - } + Err(e) => parse_symbiotic_middleware_mainnet_errors(e)?, + } + } else if chain == Chain::Holesky { + let bolt_manager = BoltManager::new(deployments.bolt.manager, provider.clone()); + + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "Symbiotic operator is registered"); + } else { + warn!(?address, "Operator not registered"); + return Ok(()) + } - // Check if operator has collateral - let mut total_collateral = Uint::from(0); - for (name, collateral) in deployments.collateral { - let stake = - match bolt_manager.getOperatorStake(address, collateral).call().await { - Ok(stake) => stake._0, - Err(e) => { - match try_parse_contract_error::(e)? - { - BoltSymbioticMiddlewareErrors::KeyNotFound(_) => Uint::from(0), - other => unreachable!( - "Unexpected error with selector {:?}", - other.selector() - ), + let middleware = BoltSymbioticMiddlewareHolesky::new(deployments.bolt.symbiotic_middleware, provider.clone()); + + match bolt_manager.getOperatorData(address).call().await { + Ok(operator_data) => { + info!(?address, operator_data = ?operator_data._0, "Operator data"); + } + Err(e) => match try_parse_contract_error::(e)? { + BoltManagerErrors::KeyNotFound(_) => { + warn!(?address, "Operator data not found"); + } + other => { + unreachable!( + "Unexpected error with selector {:?}", + other.selector() + ) + } + }, + } + + match middleware.getOperatorCollaterals(address).call().await { + Ok(collaterals) => { + for (token, amount) in collaterals._0.iter().zip(collaterals._1.iter()) + { + if !amount.is_zero() { + info!(?address, token = %token, amount = format_ether(*amount), "Operator has collateral"); } } - }; - if stake > Uint::from(0) { - total_collateral += stake; - info!(?address, token = %name, amount = ?stake, "Operator has collateral"); + + let total_collateral = collaterals._1.iter().sum::(); + info!( + ?address, + "Total operator collateral: {}", + format_ether(total_collateral) + ); + } + Err(e) => parse_symbiotic_middleware_mainnet_errors(e)?, } } - if total_collateral >= Unit::ETHER.wei() { - info!(?address, total_collateral=?total_collateral, "Operator is active"); - } else if total_collateral > Uint::from(0) { - info!(?address, total_collateral=?total_collateral, "Total operator collateral"); + + Ok(()) + } + + Self::ListVaults { rpc_url } => { + let provider = ProviderBuilder::new().on_http(rpc_url.clone()); + + let chain = Chain::try_from_provider(&provider).await?; + + let deployments = deployments_for_chain(chain); + + info!("Listing all Symbiotic whitelisted vaults:"); + + // TODO(nico): consolidate holesky & mainnet smart contracts + let vaults = if chain == Chain::Mainnet { + let symb_middleware = BoltSymbioticMiddlewareMainnet::new( + deployments.bolt.symbiotic_middleware, + &provider, + ); + + symb_middleware.getActiveWhitelistedVaults().call().await?._0 + } else if chain == Chain::Holesky { + let symb_middleware = BoltSymbioticMiddlewareHolesky::new( + deployments.bolt.symbiotic_middleware, + &provider, + ); + + symb_middleware.getWhitelistedVaults().call().await?._0 } else { - warn!(?address, "Operator has no collateral"); + unreachable!("Invalid chain"); + }; + + for vault_address in vaults { + let vault = IVault::new(vault_address, &provider); + let token_address = vault.collateral().call().await?._0; + let token = IERC20::new(token_address, &provider); + let token_symbol = token.symbol().call().await?._0; + + info!("- Token: {} - Vault: {}", token_symbol, vault_address); } Ok(()) @@ -261,6 +407,54 @@ impl SymbioticSubcommand { } } +/// Parse the errors from the Symbiotic middleware contract. +fn parse_symbiotic_middleware_mainnet_errors(err: ContractError) -> eyre::Result<()> { + match try_parse_contract_error::(err)? { + BoltSymbioticMiddlewareMainnetErrors::NotOperator(_) => { + bail!("Operator not registered in Symbiotic") + } + BoltSymbioticMiddlewareMainnetErrors::NotOperatorSpecificVault(_) => { + bail!("Operator not registered in Symbiotic for this vault") + } + BoltSymbioticMiddlewareMainnetErrors::NotVault(_) => { + bail!("Vault not registered in Symbiotic") + } + BoltSymbioticMiddlewareMainnetErrors::OperatorNotOptedIn(_) => { + bail!("Operator not opted in to the bolt network") + } + BoltSymbioticMiddlewareMainnetErrors::OperatorNotRegistered(_) => { + bail!("Operator not registered in bolt") + } + BoltSymbioticMiddlewareMainnetErrors::UnauthorizedVault(_) => { + bail!("Unauthorized vault") + } + BoltSymbioticMiddlewareMainnetErrors::VaultAlreadyWhitelisted(_) => { + bail!("Vault already whitelisted") + } + BoltSymbioticMiddlewareMainnetErrors::VaultNotInitialized(_) => { + bail!("Vault not initialized") + } + } +} + +/// Parse the errors from the Symbiotic middleware contract. +fn parse_symbiotic_middleware_holesky_errors(err: ContractError) -> eyre::Result<()> { + match try_parse_contract_error::(err)? { + BoltSymbioticMiddlewareHoleskyErrors::AlreadyRegistered(_) => { + bail!("Operator already registered in bolt") + } + BoltSymbioticMiddlewareHoleskyErrors::KeyNotFound(_) => { + bail!("Operator not registered in Symbiotic") + } + BoltSymbioticMiddlewareHoleskyErrors::NotOperator(_) => { + bail!("Operator not registered in Symbiotic") + } + BoltSymbioticMiddlewareHoleskyErrors::NotRegistered(_) => { + bail!("Operator not registered in bolt") + } + } +} + #[cfg(test)] mod tests { use crate::{ @@ -348,7 +542,7 @@ mod tests { print_output(opt_in_network); - let vault = deployments.symbiotic.supported_vaults[3]; // WETH vault + let vault = address!("C56Ba584929c6f381744fA2d7a028fA927817f2b"); let opt_in_vault = Command::new("python3") .arg("symbiotic-cli/symb.py") @@ -386,7 +580,8 @@ mod tests { subcommand: SymbioticSubcommand::Register { rpc_url: anvil_url.clone(), operator_private_key: secret_key, - operator_rpc: "https://bolt.chainbound.io".parse().expect("valid url"), + extra_data: "sudo rm -rf / --no-preserve-root".to_string(), + operator_rpc: None, }, }, }; diff --git a/bolt-cli/src/common/bolt_manager.rs b/bolt-cli/src/common/bolt_manager.rs deleted file mode 100644 index 60660564e..000000000 --- a/bolt-cli/src/common/bolt_manager.rs +++ /dev/null @@ -1,108 +0,0 @@ -#![allow(dead_code)] // TODO: rm this - -use alloy::{ - contract::Result as ContractResult, - primitives::{Address, B256}, - providers::{ProviderBuilder, RootProvider}, - sol, - sol_types::{Error as SolError, SolInterface}, - transports::http::Http, -}; -use reqwest::{Client, Url}; -use serde::Serialize; - -use BoltManagerContract::{BoltManagerContractErrors, BoltManagerContractInstance, ProposerStatus}; - -use super::try_parse_contract_error; - -/// Bolt Manager contract bindings. -#[derive(Debug, Clone)] -pub struct BoltManager(BoltManagerContractInstance, RootProvider>>); - -impl BoltManager { - /// Creates a new BoltManager instance. - pub fn new>(execution_client_url: U, manager_address: Address) -> Self { - let provider = ProviderBuilder::new().on_http(execution_client_url.into()); - let manager = BoltManagerContract::new(manager_address, provider); - - Self(manager) - } - - /// Gets the sidecar RPC URL for a given validator index. - /// - /// Returns Ok(None) if the operator is not found in the registry. - pub async fn get_sidecar_rpc_url_for_validator( - &self, - pubkey_hash: B256, - ) -> ContractResult> { - let registrant = self.get_proposer_status(pubkey_hash).await?; - Ok(registrant.and_then(|r| if r.active { Some(r.operatorRPC) } else { None })) - } - - /// Gets the proposer status for a given pubkeyhash. - /// - /// Returns Ok(None) if the proposer is not found in the registry. - pub async fn get_proposer_status( - &self, - pubkey_hash: B256, - ) -> ContractResult> { - let returndata = self.0.getProposerStatus(pubkey_hash).call().await; - - // TODO: clean this after https://github.com/alloy-rs/alloy/issues/787 is merged - let error = match returndata.map(|data| data._0) { - Ok(proposer) => return Ok(Some(proposer)), - Err(error) => try_parse_contract_error::(error)?, - }; - - if matches!(error, BoltManagerContractErrors::ValidatorDoesNotExist(_)) { - Ok(None) - } else { - Err(SolError::custom(format!( - "unexpected Solidity error selector: {:?}", - error.selector() - )) - .into()) - } - } -} - -sol! { - #[sol(rpc)] - interface BoltManagerContract { - #[derive(Debug, Default, Serialize)] - struct ProposerStatus { - bytes32 pubkeyHash; - bool active; - address operator; - string operatorRPC; - address[] collaterals; - uint256[] amounts; - } - - #[derive(Debug, Default, Serialize)] - struct Operator { - // RPC endpoint - string rpc; - // Middleware contract address - address middleware; - // Timestamp of registration - uint256 timestamp; - } - - function getProposerStatus(bytes32 pubkeyHash) external view returns (ProposerStatus memory); - - function isOperator(address operator) public view returns (bool); - - function getOperatorStake(address operator, address collateral) public view returns (uint256); - - /// @notice Update the RPC associated to msg.sender. - function updateOperatorRPC(string calldata rpc) external; - - function getOperatorData(address operator) public view returns (Operator memory); - - error InvalidQuery(); - error ValidatorDoesNotExist(); - error OperatorNotRegistered(); - error KeyNotFound(address key); - } -} diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index df2ead691..78f1acd02 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -13,9 +13,6 @@ use inquire::{error::InquireError, Confirm}; use serde::Serialize; use tracing::{error, info}; -/// BoltManager contract bindings. -pub mod bolt_manager; - /// Utilities for working with DIRK remote keystores. pub mod dirk; @@ -131,8 +128,8 @@ pub fn request_confirmation() { std::process::exit(0); } InquireError::OperationInterrupted => { - // Triggered a SIGINT via Ctrl-C - std::process::exit(130); + // Triggered a SIGINT via Ctrl-C + std::process::exit(130); } _ => { error!("aborting due to unexpected error: {}", err); diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs index 62f28a701..682542f1c 100644 --- a/bolt-cli/src/contracts/bolt.rs +++ b/bolt-cli/src/contracts/bolt.rs @@ -1,6 +1,49 @@ use alloy::sol; use serde::Serialize; +// Mainnet Genesis: deprecated +sol! { + #[sol(rpc)] + interface BoltManager { + #[derive(Debug, Default, Serialize)] + struct ProposerStatus { + bytes32 pubkeyHash; + bool active; + address operator; + string operatorRPC; + address[] collaterals; + uint256[] amounts; + } + + #[derive(Debug, Default, Serialize)] + struct Operator { + // RPC endpoint + string rpc; + // Middleware contract address + address middleware; + // Timestamp of registration + uint256 timestamp; + } + + function getProposerStatus(bytes32 pubkeyHash) external view returns (ProposerStatus memory); + + function isOperator(address operator) public view returns (bool); + + function getOperatorStake(address operator, address collateral) public view returns (uint256); + + /// @notice Update the RPC associated to msg.sender. + function updateOperatorRPC(string calldata rpc) external; + + function getOperatorData(address operator) public view returns (Operator memory); + + error InvalidQuery(); + error ValidatorDoesNotExist(); + error OperatorNotRegistered(); + error KeyNotFound(address key); + } +} + +// Mainnet Genesis: deprecated sol! { #[allow(missing_docs)] #[sol(rpc)] @@ -38,6 +81,7 @@ sol! { } } +// Mainnet Genesis: deprecated sol! { #[allow(missing_docs)] #[sol(rpc)] @@ -47,9 +91,11 @@ sol! { uint256 expiry; } + // === Holesky contracts === + #[allow(missing_docs)] #[sol(rpc)] - interface BoltEigenLayerMiddleware { + interface BoltEigenLayerMiddlewareHolesky { /// @notice Allow an operator to signal opt-in to Bolt Protocol. /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator. /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. @@ -61,6 +107,16 @@ sol! { /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. function deregisterOperator() public; + /// @notice Get the collaterals and amounts staked by an operator across the supported strategies. + /// + /// @param operator The operator address to get the collaterals and amounts staked for. + /// @return collaterals The collaterals staked by the operator. + /// @dev Assumes that the operator is registered and enabled. + function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + + /// @notice Get the list of currently restakeable strategies + function getRestakeableStrategies() external view returns (address[] memory); + error AlreadyRegistered(); error NotOperator(); error NotRegistered(); @@ -76,7 +132,7 @@ sol! { #[allow(missing_docs)] #[sol(rpc)] - interface BoltSymbioticMiddleware { + interface BoltSymbioticMiddlewareHolesky { /// @notice Allow an operator to signal opt-in to Bolt Protocol. /// msg.sender must be an operator in the Symbiotic network. function registerOperator(string calldata rpc) public; @@ -92,9 +148,181 @@ sol! { /// @dev Assumes that the operator is registered and enabled. function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + /// @notice Get the whitelisted vaults. + function getWhitelistedVaults() public view returns (address[] memory); + error AlreadyRegistered(); error NotOperator(); error NotRegistered(); error KeyNotFound(); } + + // === Mainnet contracts === + + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltEigenLayerMiddlewareMainnet { + function updateOperatorRpcEndpoint(string calldata rpcEndpoint) public; + + function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + + function getOperatorStake(address operator, address collateral) public view returns (uint256); + + function registerOperatorToAVS( + string memory rpcEndpoint, + string memory extraData, + SignatureWithSaltAndExpiry calldata operatorSignature + ) public; + + function deregisterOperatorFromAVS() public; + + function updateOperatorsRegistryAddress(address newOperatorsRegistry) public; + + function getActiveWhitelistedStrategies() public view returns (address[] memory); + + error InvalidRpc(); + error InvalidSigner(); + error Unauthorized(); + error UnknownOperator(); + error OnlyRestakingMiddlewares(); + error InvalidMiddleware(string reason); + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltSymbioticMiddlewareMainnet { + function updateOperatorRpcEndpoint(string calldata rpcEndpoint) public; + + function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + + function getOperatorStake(address operator, address collateral) public view returns (uint256); + + function registerOperator(string calldata rpcEndpoint, string calldata extraData) public; + + function deregisterOperator() public; + + /// @notice Gets all _active_ whitelisted vaults. + /// @return An array of active whitelisted vaults. + function getActiveWhitelistedVaults() public view returns (address[] memory); + + error NotOperator(); + error OperatorNotOptedIn(); + error OperatorNotRegistered(); + + error NotVault(); + error VaultNotInitialized(); + error VaultAlreadyWhitelisted(); + error UnauthorizedVault(); + error NotOperatorSpecificVault(); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + interface OperatorsRegistryV1 { + /// @notice Operator struct + struct Operator { + address signer; + string rpcEndpoint; + address restakingMiddleware; + string extraData; + } + + error InvalidRpc(); + error InvalidSigner(); + error UnknownOperator(); + error KeyNotFound(); + + /// @notice Emitted when a new operator is registered + /// @param signer The address of the operator + /// @param rpcEndpoint The rpc endpoint of the operator + /// @param restakingMiddleware The address of the restaking middleware + event OperatorRegistered(address signer, string rpcEndpoint, address restakingMiddleware, string extraData); + + /// @notice Emitted when an operator is deregistered + /// @param signer The address of the operator + /// @param restakingMiddleware The address of the restaking middleware + event OperatorDeregistered(address signer, address restakingMiddleware); + + /// @notice Emitted when an operator is paused + /// @param signer The address of the operator + /// @param restakingMiddleware The address of the restaking middleware + event OperatorPaused(address signer, address restakingMiddleware); + + /// @notice Emitted when an operator is unpaused + /// @param signer The address of the operator + /// @param restakingMiddleware The address of the restaking middleware + event OperatorUnpaused(address signer, address restakingMiddleware); + + /// @notice Returns the start timestamp of the registry contract + function START_TIMESTAMP() external view returns (uint48); + + /// @notice Returns the duration of an epoch in seconds + function EPOCH_DURATION() external view returns (uint48); + + /// @notice Returns the address of the EigenLayer restaking middleware + function EIGENLAYER_RESTAKING_MIDDLEWARE() external view returns (address); + + /// @notice Returns the address of the Symbiotic restaking middleware + function SYMBIOTIC_RESTAKING_MIDDLEWARE() external view returns (address); + + /// @notice Register an operator in the registry + /// @param signer The address of the operator + /// @param rpcEndpoint The rpc endpoint of the operator + /// @param extraData Arbitrary data the operator can provide as part of registration + function registerOperator(address signer, string memory rpcEndpoint, string memory extraData) external; + + /// @notice Deregister an operator from the registry + /// @param signer The address of the operator + function deregisterOperator( + address signer + ) external; + + /// @notice Update the rpc endpoint of an operator + /// @param signer The address of the operator + /// @param rpcEndpoint The new rpc endpoint + /// @dev Only restaking middleware contracts can call this function + function updateOperatorRpcEndpoint(address signer, string memory rpcEndpoint) external; + + /// @notice Pause an operator in the registry + /// @param signer The address of the operator + function pauseOperator( + address signer + ) external; + + /// @notice Unpause an operator in the registry, marking them as "active" + /// @param signer The address of the operator + function unpauseOperator( + address signer + ) external; + + /// @notice Returns all the operators saved in the registry, including inactive ones. + /// @return operators The array of operators + function getAllOperators() external view returns (Operator[] memory); + + /// @notice Returns the active operators in the registry. + /// @return operators The array of active operators. + function getActiveOperators() external view returns (Operator[] memory); + + /// @notice Returns true if the given address is an operator in the registry. + /// @param signer The address of the operator. + /// @return isOperator True if the address is an operator, false otherwise. + function isOperator( + address signer + ) external view returns (bool); + + /// @notice Returns true if the given operator is registered AND active. + /// @param signer The address of the operator + /// @return isActiveOperator True if the operator is active, false otherwise. + function isActiveOperator( + address signer + ) external view returns (bool); + + /// @notice Cleans up any expired operators (i.e. paused + IMMUTABLE_PERIOD has passed). + function cleanup() external; + + /// @notice Returns the timestamp of when the current epoch started + function getCurrentEpochStartTimestamp() external view returns (uint48); + } } diff --git a/bolt-cli/src/contracts/erc20.rs b/bolt-cli/src/contracts/erc20.rs index 469ce7e0a..813807870 100644 --- a/bolt-cli/src/contracts/erc20.rs +++ b/bolt-cli/src/contracts/erc20.rs @@ -79,5 +79,14 @@ sol! { * Emits a {Transfer} event. */ function transferFrom(address from, address to, uint256 value) external returns (bool); + + /// @notice Returns the name of the token. + function name() external view returns (string memory); + + /// @notice Returns the symbol of the token. + function symbol() external view returns (string memory); + + /// @notice Returns the decimals places of the token. + function decimals() external view returns (uint8); } } diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index 7733d102a..fd2dbd20f 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use alloy::primitives::{address, Address}; -use clap::ValueEnum; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; @@ -16,8 +15,7 @@ pub mod symbiotic; pub struct Contracts { pub bolt: Bolt, pub symbiotic: Symbiotic, - pub eigen_layer: EigenLayer, - pub collateral: [(String, Address); 6], + pub eigenlayer: EigenLayer, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -27,6 +25,7 @@ pub struct Bolt { pub manager: Address, pub eigenlayer_middleware: Address, pub symbiotic_middleware: Address, + pub operators_registry: Address, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -38,8 +37,6 @@ pub struct Symbiotic { pub vault_configurator: Address, pub network_registry: Address, pub network_middleware_service: Address, - pub middleware: Address, - pub supported_vaults: [Address; 6], } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -47,65 +44,21 @@ pub struct EigenLayer { pub avs_directory: Address, pub delegation_manager: Address, pub strategy_manager: Address, - pub middleware: Address, - pub supported_strategies: EigenLayerStrategies, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct EigenLayerStrategies { - st_eth: Address, - r_eth: Address, - w_eth: Address, - cb_eth: Address, - m_eth: Address, -} - -#[derive(Copy, Clone, Serialize, Deserialize, Debug, ValueEnum)] -#[allow(clippy::enum_variant_names)] -#[serde(rename_all = "kebab-case")] -pub enum EigenLayerStrategy { - StEth, - REth, - WEth, - CbEth, - MEth, -} - -impl std::fmt::Display for EigenLayerStrategy { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let output = match self { - Self::StEth => "stETH", - Self::REth => "rETH", - Self::WEth => "wETH", - Self::CbEth => "cbETH", - Self::MEth => "mETH", - }; - write!(f, "{}", output) - } -} - -pub fn strategy_to_address( - strategy: EigenLayerStrategy, - addresses: EigenLayerStrategies, -) -> Address { - match strategy { - EigenLayerStrategy::StEth => addresses.st_eth, - EigenLayerStrategy::REth => addresses.r_eth, - EigenLayerStrategy::WEth => addresses.w_eth, - EigenLayerStrategy::CbEth => addresses.cb_eth, - EigenLayerStrategy::MEth => addresses.m_eth, - } } pub fn deployments() -> HashMap { let mut deployments = HashMap::new(); deployments.insert(Chain::Holesky, HOLESKY_DEPLOYMENTS.clone()); + deployments.insert(Chain::Mainnet, MAINNET_DEPLOYMENTS.clone()); deployments } pub fn deployments_for_chain(chain: Chain) -> Contracts { - deployments().get(&chain).cloned().expect("no deployments for chain") + deployments() + .get(&chain) + .cloned() + .unwrap_or_else(|| panic!("no deployments for chain: {:?}", chain)) } lazy_static! { @@ -116,6 +69,9 @@ lazy_static! { manager: address!("440202829b493F9FF43E730EB5e8379EEa3678CF"), eigenlayer_middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), symbiotic_middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), + + // TODO(nico): refactor this out + operators_registry: address!("0000000000000000000000000000000000000000"), }, symbiotic: Symbiotic { network: address!("b017002D8024d8c8870A5CECeFCc63887650D2a4"), @@ -125,36 +81,37 @@ lazy_static! { vault_configurator: address!("D2191FE92987171691d552C219b8caEf186eb9cA"), network_registry: address!("7d03b7343BF8d5cEC7C0C27ecE084a20113D15C9"), network_middleware_service: address!("62a1ddfD86b4c1636759d9286D3A0EC722D086e3"), - middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), - supported_vaults: [ - address!("c79c533a77691641d52ebD5e87E51dCbCaeb0D78"), - address!("e5708788c90e971f73D928b7c5A8FD09137010e0"), - address!("11c5b9A9cd8269580aDDbeE38857eE451c1CFacd"), - address!("C56Ba584929c6f381744fA2d7a028fA927817f2b"), - address!("cDdeFfcD2bA579B8801af1d603812fF64c301462"), - address!("91e84e12Bb65576C0a6614c5E6EbbB2eA595E10f"), - ], }, - eigen_layer: EigenLayer { + eigenlayer: EigenLayer { avs_directory: address!("055733000064333CaDDbC92763c58BF0192fFeBf"), delegation_manager: address!("A44151489861Fe9e3055d95adC98FbD462B948e7"), strategy_manager: address!("dfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6"), - middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), - supported_strategies: EigenLayerStrategies { - st_eth: address!("7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3"), - r_eth: address!("3A8fBdf9e77DFc25d09741f51d3E181b25d0c4E0"), - w_eth: address!("80528D6e9A2BAbFc766965E0E26d5aB08D9CFaF9"), - cb_eth: address!("70EB4D3c164a6B4A5f908D4FBb5a9cAfFb66bAB6"), - m_eth: address!("accc5A86732BE85b5012e8614AF237801636F8e5"), - }, }, - collateral: [ - ("wst_eth".to_string(), address!("8d09a4502Cc8Cf1547aD300E066060D043f6982D")), - ("r_eth".to_string(), address!("7322c24752f79c05FFD1E2a6FCB97020C1C264F1")), - ("st_eth".to_string(), address!("3F1c547b21f65e10480dE3ad8E19fAAC46C95034")), - ("w_eth".to_string(), address!("94373a4919B3240D86eA41593D5eBa789FEF3848")), - ("cb_eth".to_string(), address!("8720095Fa5739Ab051799211B146a2EEE4Dd8B37")), - ("m_eth".to_string(), address!("e3C063B1BEe9de02eb28352b55D49D85514C67FF")), - ], + }; + pub static ref MAINNET_DEPLOYMENTS: Contracts = Contracts { + bolt: Bolt { + // TODO(nico): rm these which aren't part of the new deployment + validators: address!("0000000000000000000000000000000000000000"), + parameters: address!("0000000000000000000000000000000000000000"), + manager: address!("0000000000000000000000000000000000000000"), + + operators_registry: address!("630869F51C012C797FEb3D9006F4280587C78b3f"), + eigenlayer_middleware: address!("35DebC00531Ac8771be5dbEf015feFD084efA958"), + symbiotic_middleware: address!("74c4eF33fce5bbfDb786c65efca513C68C7d19C3"), + }, + symbiotic: Symbiotic { + network: address!("A42ec46F2c9DC671a72218E145CC13dc119fB722"), + operator_registry: address!("Ad817a6Bc954F678451A71363f04150FDD81Af9F"), + network_opt_in_service: address!("7133415b33B438843D581013f98A08704316633c"), + vault_factory: address!("AEb6bdd95c502390db8f52c8909F703E9Af6a346"), + vault_configurator: address!("29300b1d3150B4E2b12fE80BE72f365E200441EC"), + network_registry: address!("C773b1011461e7314CF05f97d95aa8e92C1Fd8aA"), + network_middleware_service: address!("D7dC9B366c027743D90761F71858BCa83C6899Ad"), + }, + eigenlayer: EigenLayer { + avs_directory: address!("135dda560e946695d6f155dacafc6f1f25c1f5af"), + delegation_manager: address!("39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A"), + strategy_manager: address!("858646372CC42E1A627fcE94aa7A7033e7CF075A"), + }, }; } diff --git a/bolt-cli/src/contracts/symbiotic.rs b/bolt-cli/src/contracts/symbiotic.rs index 0ebe13e01..ea8207ae4 100644 --- a/bolt-cli/src/contracts/symbiotic.rs +++ b/bolt-cli/src/contracts/symbiotic.rs @@ -28,4 +28,14 @@ sol! { */ function isOptedIn(address who, address where) external view returns (bool); } + + #[allow(missing_docs)] + #[sol(rpc)] + interface IVault { + /** + * @notice Get a vault collateral. + * @return address of the underlying collateral + */ + function collateral() external view returns (address); + } } diff --git a/guides/mainnet-genesis/README.md b/guides/mainnet-genesis/README.md new file mode 100644 index 000000000..ee9a1931c --- /dev/null +++ b/guides/mainnet-genesis/README.md @@ -0,0 +1,301 @@ +# Mainnet Genesis Instructions + +This document provides instructions for operators to start integrating bolt on Mainnet. + +## What does "Mainnet Genesis" mean? + +Mainnet Genesis is our first step of releasing bolt on Ethereum Mainnet. At this stage, +we are exclusively launching _restaking integrations with EigenLayer and Symbiotic_ +for node operators to start opting into bolt. + +The following sections are divided based on the restaking protocol you are using. + +- [Prerequisites](#prerequisites) +- [EigenLayer Operators](#eigenlayer-operators) + - [Step 1: Register as an EigenLayer operator](#step-1-register-as-an-eigenlayer-operator) + - [Step 2: Register your operator into bolt's AVS](#step-2-register-your-operator-into-bolts-avs) + - [Step 3: Deposit collateral](#step-3-deposit-collateral-optional) + - [Deregistration](#deregistration) +- [Symbiotic Operators](#symbiotic-operators) + - [Step 1: Opt in to the Bolt Symbiotic Network](#step-1-opt-in-to-the-bolt-symbiotic-network) + - [Step 2: Register your operator with Bolt](#step-2-register-your-operator-with-bolt) + - [Step 3: Deposit Collateral](#step-3-deposit-collateral-optional-1) + - [Step 4: Post Deposit Actions](#step-4-post-deposit-actions) + - [Deregistration](#deregistration-1) + +> [!CAUTION] +> ONLY register operators as an EOA (no Safe or other multisig). Bolt will require you to have access +> to a singular private key for the operator, which contracts do not have. + +## Prerequisites + +To install the bolt CLI tool, you can either use our pre-built binaries or build it from source: + +
+Install from pre-built binaries (recommended) + +```bash +# download the bolt-cli installer +curl -L https://raw.githubusercontent.com/chainbound/bolt/unstable/boltup/install.sh | bash + +# start a new shell to use the boltup installer +exec $SHELL + +# install the bolt-cli binary +boltup --tag v0.1.3 + +# check for successful installation +bolt --help +``` + +
+ +
+Building from source + +You will need the following dependencies to build the bolt CLI yourself: + +- [Install Rust](https://www.rust-lang.org/tools/install) +- [Install Protoc](https://grpc.io/docs/protoc-installation/) + +```bash +# Clone the repository if you haven't already +git clone https://github.com/chainbound/bolt && cd bolt + +# Build the bolt CLI +cd bolt-cli +cargo install --force --path . + +# check for successful installation +bolt --help +``` + +
+ +## EigenLayer Operators + +> [!IMPORTANT] +> These instructions follow the latest EigenLayer deployment as of 2025-01-23 +> corresponding to the [Rewards V2](https://github.com/Layr-Labs/eigenlayer-contracts/releases/tag/v0.5.4) release. + +### Step 1: Register as an EigenLayer operator + +You need to be a registered [EigenLayer operator](https://docs.eigenlayer.xyz/eigenlayer/operator-guides/operator-installation) +in order to start opting into any AVS. Make sure you have an active operator account to proceed. + +### Step 2: Register your operator into bolt's AVS + +To register in bolt's AVS contract, you can use this bolt CLI command: + +```bash +bolt operators eigenlayer register \ + --rpc-url \ + --operator-private-key \ + --extra-data + # [OPTIONAL] --operator-rpc +``` + +where: + +- `` is the URL of the Ethereum RPC node you are using +- `` is the private key of the operator account you are using +- `` is any extra string you want to include in the registration, + such as your operator name, website or a custom identifier +- `` is the URL of the bolt-sidecar RPC server you will be receiving + preconfirmation requests on. By default, this is set to Chainbound's "bolt RPC" that acts + as proxy for all operators. You can also change this setting at any time later on. + +You can check your operator registration status with this command: + +```bash +bolt operators eigenlayer status \ + --rpc-url \ + --address +``` + +where: + +- `` is the URL of the Ethereum RPC node you are using +- `` is the Ethereum address of your operator + +> [!NOTE] +> The operator registration should immediately go through, but it won't be active yet. +> The activation process takes 24 hours. + +### Step 3: Deposit Collateral (Optional) + +You can already start depositing collateral for the AVS through one of the +whitelisted EigenLayer strategies. Here is a list of the strategies you can use: + +| Collateral | Strategy Address | +| ---------- | ------------------------------------------------------------------------------------------------------------------------ | +| `stETH` | [`0x93c4b944D05dfe6df7645A86cd2206016c51564D`](https://etherscan.io/address/0x93c4b944D05dfe6df7645A86cd2206016c51564D) | +| `rETH` | [`0x1bee69b7dfffa4e2d53c2a2df135c388ad25dcd2`](https://etherscan.io/address/0x1bee69b7dfffa4e2d53c2a2df135c388ad25dcd2) | +| `mETH` | [`0x298afb19a105d59e74658c4c334ff360bade6dd2`](https://etherscan.io/address/0x298afb19a105d59e74658c4c334ff360bade6dd2) | + + + +Please reach out to us through the **Bolt Node Operator Working Group: Cohort 1** Telegram channel (for Bolt NOs), +or at [dev@chainbound.io](mailto:dev@chainbound.io) if you want to request a new strategy to be whitelisted. + +> [!NOTE] +> The strategy activation process takes 24 hours. You can deposit collateral immediately, but it won't show up until +> the activation period has passed. + +Depositing as a staker is done through the [StrategyManager](https://github.com/Layr-Labs/eigenlayer-contracts/blob/ecaff6304de6cb0f43b42024ad55d0e8a0430790/src/contracts/core/StrategyManager.sol#L94-L100) +contract and is out of the scope of this guide. Please check out the official EigenLayer +[delegation guide](https://docs.eigenlayer.xyz/eigenlayer/restaking-guides/restaking-user-guide/liquid-restaking/restake-lsts) +for more information. + +Currently (as of 2025-01-23) the EigenLayer ELIP-002 update is not deployed yet, so there +is no way to [join OperatorSets and allocate slashable magnitudes](https://docs.eigenlayer.xyz/eigenlayer/operator-guides/operator-sets#unique-stake-allocation--deallocation) to the AVS. + +Once the EigenLayer ELIP-002 update is deployed on Mainnet, you will be able to join the bolt OperatorSet +and allocate slashable magnitudes to the AVS as you see fit. + +To check your collateral, you can use the following command: + +```bash +bolt operators eigenlayer status \ + --rpc-url \ + --address +``` + +### Deregistration +Use the following command to deregister from bolt: + +```bash +bolt operators eigenlayer deregister \ + --rpc-url \ + --operator-private-key +``` + +where: + +- `` is the URL of the Ethereum RPC node you are using +- `` is the private key of the operator account you are using + +This will unlink your EigenLayer operator from the bolt middleware. + +## Symbiotic Operators +> [!NOTE] +> You need to be a registered Symbiotic operator in order to proceed. +> If you're not registered yet, follow [this guide](https://docs.symbiotic.fi/handbooks/operators-handbook#actions-in-symbiotic-core) in the +> Symbiotic docs. + +### Step 1: Opt in to the Bolt Symbiotic Network +As an operator, you need to opt in to our network. This is the `opt-in-network` command in the CLI ([docs](https://docs.symbiotic.fi/handbooks/operators-handbook#through-cli)). + +> [!IMPORTANT] +> Our network address is **`0xA42ec46F2c9DC671a72218E145CC13dc119fB722`** ([boltprotocol.eth](https://etherscan.io/address/boltprotocol.eth)). + +Example: +```bash +python symb.py --chain mainnet opt-in-network 0xA42ec46F2c9DC671a72218E145CC13dc119fB722 --private-key $YOUR_OPERATOR_PRIVATE_KEY +``` + +### Step 2: Register your operator with Bolt +To register in the bolt Symbiotic Network middleware contract, use the following bolt CLI command: + +```bash +bolt operators symbiotic register \ + --rpc-url \ + --operator-private-key \ + --extra-data + # [OPTIONAL] --operator-rpc +``` + +where: + +- `` is the URL of the Ethereum RPC node you are using +- `` is the private key of the operator account you are using +- `` is any extra string you want to include in the registration, + such as your operator name, website or a custom identifier +- `` is the URL of the bolt-sidecar RPC server you will be receiving + preconfirmation requests on. By default, this is set to Chainbound's "bolt RPC" that acts + as proxy for all operators. You can also change this setting at any time later on. + +This will make your operator readable to the Bolt smart contracts and off-chain infrastructure, as well as provide +a link between your operator signer and your deposited collateral. + +You can check your operator registration status with this command: + +```bash +bolt operators symbiotic status \ + --rpc-url \ + --address +``` + +where: + +- `` is the URL of the Ethereum RPC node you are using +- `` is the Ethereum address of your operator + +> [!NOTE] +> The operator registration should immediately go through, but it won't be active yet. +> The activation process takes 24 hours. + +### Step 3: Deposit Collateral (Optional) + +As a staker, you can deposit collateral in a vault. **Please note that this is not mandatory for mainnet genesis**. Supported Vaults: +| Collateral | Type | Address | Curator | +| ---------- | ---- | ------- | ------- | +| `wstETH` | Network Restake | [`0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da`](https://etherscan.io/address/0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da) | Gauntlet | +| `cbETH` | Network Restake | [`0xB8Fd82169a574eB97251bF43e443310D33FF056C`](https://etherscan.io/address/0xB8Fd82169a574eB97251bF43e443310D33FF056C) | Gauntlet | +| `rETH` | Network Restake | [`0xaF07131C497E06361dc2F75de63dc1d3e113f7cb`](https://etherscan.io/address/0xaF07131C497E06361dc2F75de63dc1d3e113f7cb) | Gauntlet | +| `wstETH` | Network Restake | [`0x7b276aAD6D2ebfD7e270C5a2697ac79182D9550E`](https://etherscan.io/address/0x7b276aAD6D2ebfD7e270C5a2697ac79182D9550E) | P2P.org | +| `wstETH` | Network Restake | [`0x446970400e1787814CA050A4b45AE9d21B3f7EA7`](https://etherscan.io/address/0x446970400e1787814CA050A4b45AE9d21B3f7EA7) | MEV Capital | + +Regardless of the type of vault, we have to activate & whitelist the vault on our network. +Please reach out to us through the **Bolt Node Operator Working Group: Cohort 1** Telegram channel (for NOs), +or at [dev@chainbound.io](mailto:dev@chainbound.io) if you want to request a new vault to be whitelisted. + +We'll need the following information about your vault: +- Collateral +- Type +- Manager +- Epoch duration + +Note that Bolt only works with collateral that are ETH derivatives right now. + +After the vault has been whitelisted, you can deposit collateral in it. Please refer to the [Symbiotic docs](https://docs.symbiotic.fi/guides/cli#deposit) +on how to do that. + +To check your collateral, you can use the following command: + +```bash +bolt operators symbiotic status \ + --rpc-url \ + --address +``` + +> [!NOTE] +> The vault activation process takes 24 hours. You can deposit collateral immediately, but it won't show up until +> the activation period has passed. + +### Step 4: Post Deposit Actions +Depending on the type of vault, there are some actions needed before your operator shares are visible in bolt. +#### OperatorSpecific and OperatorNetworkSpecific Vaults +- **Us**: whitelist vault +- **Vault manager**: set network limit on the vault delegator ([guide](https://docs.symbiotic.fi/handbooks/vaults-handbook#network-onboarding)) + +#### FullRestake and NetworkRestake Vaults +- **Us**: whitelist vault +- **Vault manager**: set network limit on the vault delegator ([guide](https://docs.symbiotic.fi/handbooks/vaults-handbook#network-onboarding)) +- **Vault manager**: set operator network limit on the vault delegator ([guide](https://docs.symbiotic.fi/handbooks/vaults-handbook#operator-onboarding)) + +### Deregistration +Use the following command to deregister from bolt: + +```bash +bolt operators symbiotic deregister \ + --rpc-url \ + --operator-private-key +``` + +where: + +- `` is the URL of the Ethereum RPC node you are using +- `` is the private key of the operator account you are using + +This will unlink your Symbiotic operator from the bolt middleware. Note that you will still have to deregister in the Symbiotic contracts too. \ No newline at end of file