diff --git a/Makefile b/Makefile index 93bf2fa..8f063ad 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ include .env GO_LINES_IGNORED_DIRS= -GO_PACKAGES=./pkg/... ./cmd/... +GO_PACKAGES=./pkg/... ./cmd/... ./internal/... GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g") help: diff --git a/internal/versionupdate/check_version_update.go b/internal/versionupdate/check_version_update.go index 6e72256..e32454a 100644 --- a/internal/versionupdate/check_version_update.go +++ b/internal/versionupdate/check_version_update.go @@ -67,7 +67,9 @@ func Check(currentVersion string) { fmt.Println() fmt.Printf("There is a new version (%s) for this library available.\n", greenVersion) fmt.Printf("Your current running verison is (%s).\n", yellowOldVersion) - fmt.Println("Please update (https://github.com/Layr-Labs/eigenlayer-cli#install-eigenlayer-cli-using-a-binary) to get latest features and bug fixes.") + fmt.Println( + "Please update (https://github.com/Layr-Labs/eigenlayer-cli#install-eigenlayer-cli-using-a-binary) to get latest features and bug fixes.", + ) fmt.Println() } } diff --git a/pkg/internal/common/flags/avs.go b/pkg/internal/common/flags/avs.go new file mode 100644 index 0000000..28c0c0e --- /dev/null +++ b/pkg/internal/common/flags/avs.go @@ -0,0 +1,12 @@ +package flags + +import "github.com/urfave/cli/v2" + +var ( + AvsAddressFlag = cli.StringFlag{ + Name: "avs-address", + Usage: "The address of the AVS", + EnvVars: []string{"AVS_ADDRESS"}, + Aliases: []string{"avsa"}, + } +) diff --git a/pkg/internal/common/flags/operator.go b/pkg/internal/common/flags/operator.go new file mode 100644 index 0000000..1b00360 --- /dev/null +++ b/pkg/internal/common/flags/operator.go @@ -0,0 +1,12 @@ +package flags + +import "github.com/urfave/cli/v2" + +var ( + OperatorAddressFlag = cli.StringFlag{ + Name: "operator-address", + Usage: "The address of the operator", + EnvVars: []string{"OPERATOR_ADDRESS"}, + Aliases: []string{"oa"}, + } +) diff --git a/pkg/internal/common/flags/operatorset.go b/pkg/internal/common/flags/operatorset.go new file mode 100644 index 0000000..fb4905c --- /dev/null +++ b/pkg/internal/common/flags/operatorset.go @@ -0,0 +1,13 @@ +package flags + +import "github.com/urfave/cli/v2" + +var ( + OperatorSetIdsFlag = cli.Uint64SliceFlag{ + Name: "operator-set-ids", + Usage: "The IDs of the operator sets to deregister. Comma separated list of operator set ids", + Required: true, + EnvVars: []string{"OPERATOR_SET_IDS"}, + Aliases: []string{"opsids"}, + } +) diff --git a/pkg/internal/common/helper.go b/pkg/internal/common/helper.go index bc461e0..da08d12 100644 --- a/pkg/internal/common/helper.go +++ b/pkg/internal/common/helper.go @@ -255,7 +255,7 @@ func ReadConfigFile(path string) (*types.OperatorConfig, error) { return nil, err } - elAVSDirectoryAddress, err := getAVSDirectoryAddress(operatorCfg.ChainId) + elAVSDirectoryAddress, err := GetAVSDirectoryAddress(operatorCfg.ChainId) if err != nil { return nil, err } @@ -270,7 +270,7 @@ func ReadConfigFile(path string) (*types.OperatorConfig, error) { return &operatorCfg, nil } -func getAVSDirectoryAddress(chainID big.Int) (string, error) { +func GetAVSDirectoryAddress(chainID big.Int) (string, error) { chainIDInt := chainID.Int64() chainMetadata, ok := utils.ChainMetadataMap[chainIDInt] if !ok { diff --git a/pkg/operator.go b/pkg/operator.go index 4fafe4f..73fcbf7 100644 --- a/pkg/operator.go +++ b/pkg/operator.go @@ -17,6 +17,7 @@ func OperatorCmd(p utils.Prompter) *cli.Command { operator.StatusCmd(p), operator.UpdateCmd(p), operator.UpdateMetadataURICmd(p), + operator.ForceDeregister(p), }, } diff --git a/pkg/operator/forcederegister.go b/pkg/operator/forcederegister.go new file mode 100644 index 0000000..9ce9d5b --- /dev/null +++ b/pkg/operator/forcederegister.go @@ -0,0 +1,252 @@ +package operator + +import ( + "errors" + "fmt" + "math/big" + "sort" + "strconv" + "strings" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" + "github.com/Layr-Labs/eigenlayer-cli/pkg/types" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" + contractIAVSDirectory "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IAVSDirectory" + "github.com/Layr-Labs/eigensdk-go/logging" + eigenMetrics "github.com/Layr-Labs/eigensdk-go/metrics" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/urfave/cli/v2" +) + +type deregisterConfig struct { + operatorSetIds []uint32 + avsAddress gethcommon.Address + operatorAddress gethcommon.Address + network string + signerConfig *types.SignerConfig + chainId *big.Int + broadcast bool + ethClient *ethclient.Client + outputType string + outputFile string +} + +func ForceDeregister(p utils.Prompter) *cli.Command { + return &cli.Command{ + Name: "force-deregister", + Usage: "Force deregisters operator from operator sets", + UsageText: "deregister [flags] ", + Description: ` +Force deregisters operator sets. This can be use to deregister from any operator set in the AVS if in case AVS does not provide a way to deregister. This does not require any signature from the AVS. + + is the address of the AVS contract + is a comma-separated list of operator set IDs to deregister from +`, + Flags: getDeregisterFlags(), + Action: func(c *cli.Context) error { + return deregisterOperatorSet(c, p) + }, + // TODO(shrimalmadhur): Add this flag when we test it + Hidden: true, + } +} + +func getDeregisterFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.OperatorAddressFlag, + &flags.VerboseFlag, + &flags.BroadcastFlag, + &flags.ETHRpcUrlFlag, + } + + allFlags := append(baseFlags, flags.GetSignerFlags()...) + sort.Sort(cli.FlagsByName(allFlags)) + return allFlags +} + +func deregisterOperatorSet(c *cli.Context, p utils.Prompter) error { + ctx := c.Context + logger := common.GetLogger(c) + + config, err := readAndValidateDeregisterConfig(c, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate force deregister config. use --help", err) + } + c.App.Metadata["network"] = config.chainId.String() + avsDirectoryAddress, err := common.GetAVSDirectoryAddress(*config.chainId) + if err != nil { + return err + } + if config.broadcast { + if config.signerConfig == nil { + return errors.New("signerConfig is required for broadcasting") + } + logger.Info("Broadcasting claim...") + keyWallet, sender, err := common.GetWallet( + *config.signerConfig, + config.operatorAddress.String(), + config.ethClient, + p, + *config.chainId, + logger, + ) + if err != nil { + return eigenSdkUtils.WrapError("failed to get wallet", err) + } + + txMgr := txmgr.NewSimpleTxManager(keyWallet, config.ethClient, logger, sender) + noopMetrics := eigenMetrics.NewNoopMetrics() + eLWriter, err := elcontracts.NewWriterFromConfig( + elcontracts.Config{ + AvsDirectoryAddress: gethcommon.HexToAddress(avsDirectoryAddress), + }, + config.ethClient, + logger, + noopMetrics, + txMgr, + ) + if err != nil { + return eigenSdkUtils.WrapError("failed to create new writer from config", err) + } + + // TODO(shrimalmadhur): contractIAVSDirectory.ISignatureUtilsSignatureWithSaltAndExpiry{} is a placeholder for + // the signature right now we are not using force deregister via signature but in future we will support it so + // that someone can force deregister on behalf of an operator + receipt, err := eLWriter.ForceDeregisterFromOperatorSets( + ctx, + config.operatorAddress, + config.avsAddress, + config.operatorSetIds, + contractIAVSDirectory.ISignatureUtilsSignatureWithSaltAndExpiry{}, + ) + if err != nil { + return eigenSdkUtils.WrapError("failed to submit force deregister transaction", err) + } + + logger.Infof("Force deregiister transaction submitted successfully") + common.PrintTransactionInfo(receipt.TxHash.String(), config.chainId) + } else { + if config.outputType == string(common.OutputType_Calldata) { + noSendTxOpts := common.GetNoSendTxOpts(config.operatorAddress) + _, _, contractBindings, err := elcontracts.BuildClients(elcontracts.Config{ + AvsDirectoryAddress: gethcommon.HexToAddress(avsDirectoryAddress), + }, config.ethClient, nil, logger, nil) + if err != nil { + return err + } + + unsignedTx, err := contractBindings.AvsDirectory.ForceDeregisterFromOperatorSets( + noSendTxOpts, + config.operatorAddress, + config.avsAddress, + config.operatorSetIds, + contractIAVSDirectory.ISignatureUtilsSignatureWithSaltAndExpiry{}, + ) + if err != nil { + return err + } + + calldataHex := gethcommon.Bytes2Hex(unsignedTx.Data()) + if !common.IsEmptyString(config.outputFile) { + err = common.WriteToFile([]byte(calldataHex), config.outputFile) + if err != nil { + return err + } + logger.Infof("Call data written to file: %s", config.outputFile) + } else { + fmt.Println(calldataHex) + } + } else { + fmt.Println("Force Deregister Operator Set") + + fmt.Println("Operator Address (required if operator is not the sender):", config.operatorAddress.String()) + fmt.Println("AVS Address:", config.avsAddress.String()) + // Convert uint32 to strings + stringSlice := make([]string, len(config.operatorSetIds)) + for i, num := range config.operatorSetIds { + stringSlice[i] = fmt.Sprint(num) + } + fmt.Println("Operator Set IDs:", strings.Join(stringSlice, ",")) + fmt.Println("To broadcast the force deregister, use the --broadcast flag") + } + } + return nil +} + +func readAndValidateDeregisterConfig(c *cli.Context, logger logging.Logger) (*deregisterConfig, error) { + // Read and validate the deregister config + network := c.String(flags.NetworkFlag.Name) + chainId := utils.NetworkNameToChainId(network) + broadcast := c.Bool(flags.BroadcastFlag.Name) + + output := c.String(flags.OutputFileFlag.Name) + outputType := c.String(flags.OutputTypeFlag.Name) + + ethRpcClient, err := ethclient.Dial(c.String(flags.ETHRpcUrlFlag.Name)) + if err != nil { + return nil, err + } + + args := c.Args().Slice() + if len(args) != 2 { + return nil, errors.New("invalid number of arguments") + } + avsAddress := gethcommon.HexToAddress(c.Args().Get(0)) + operatorSetIds, err := stringToUnit32Array(strings.Split(c.Args().Get(1), ",")) + if err != nil { + return nil, err + } + + operatorAddress := gethcommon.HexToAddress(c.String(flags.OperatorAddressFlag.Name)) + signer, err := common.GetSignerConfig(c, logger) + if err != nil { + // We don't want to throw error since people can still use it to generate the claim + // without broadcasting it + logger.Debugf("Failed to get signerConfig config: %s", err) + } + + return &deregisterConfig{ + operatorSetIds: operatorSetIds, + avsAddress: avsAddress, + operatorAddress: operatorAddress, + network: network, + signerConfig: signer, + chainId: chainId, + broadcast: broadcast, + ethClient: ethRpcClient, + outputFile: output, + outputType: outputType, + }, nil +} + +func stringToUnit32Array(arr []string) ([]uint32, error) { + // Convert a string array to an uint32 array + res := make([]uint32, len(arr)) + for i, v := range arr { + vUint, err := parseStringToUint32Array(v) + if err != nil { + return nil, err + } + res[i] = vUint + } + return res, nil +} + +func parseStringToUint32Array(s string) (uint32, error) { + // Convert a string to uint32 + parseUint, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return 0, err + } + + return uint32(parseUint), nil +}