diff --git a/go.mod b/go.mod index cbdc0230..1cf783d0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Layr-Labs/eigenlayer-contracts v0.3.2-mainnet-rewards github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12 github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e - github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241121204729-7d2cd162ffe8 + github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241211225219-79336bf6e886 github.com/blang/semver/v4 v4.0.0 github.com/consensys/gnark-crypto v0.12.1 github.com/ethereum/go-ethereum v1.14.5 @@ -19,6 +19,7 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.27.2 github.com/wagslane/go-password-validator v0.3.0 + github.com/wealdtech/go-merkletree/v2 v2.5.2-0.20240302222400-69219c450662 github.com/wk8/go-ordered-map/v2 v2.1.8 go.uber.org/mock v0.4.0 gopkg.in/yaml.v2 v2.4.0 @@ -100,7 +101,6 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect - github.com/wealdtech/go-merkletree/v2 v2.5.2-0.20240302222400-69219c450662 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect diff --git a/go.sum b/go.sum index f893523b..7d2d824e 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,8 @@ github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12 h1:G5Q1SnLmFbEjhOkky3vIHk github.com/Layr-Labs/eigenlayer-rewards-proofs v0.2.12/go.mod h1:OlJd1QjqEW53wfWG/lJyPCGvrXwWVEjPQsP4TV+gttQ= github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e h1:DvW0/kWHV9mZsbH2KOjEHKTSIONNPUj6X05FJvUohy4= github.com/Layr-Labs/eigenpod-proofs-generation v0.0.14-stable.0.20240730152248-5c11a259293e/go.mod h1:T7tYN8bTdca2pkMnz9G2+ZwXYWw5gWqQUIu4KLgC/vM= -github.com/Layr-Labs/eigensdk-go v0.1.13-0.20241023200243-565bb4438918 h1:Itl141PoMFzq58ZTo4Nu/CyH+x8f4BH6OmBNhZ6Z2/I= -github.com/Layr-Labs/eigensdk-go v0.1.13-0.20241023200243-565bb4438918/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= -github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241121204729-7d2cd162ffe8 h1:6wuVq+Elto+yF7bQ3QYqD2psxGXR3wcJh2koNcUjIQM= -github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241121204729-7d2cd162ffe8/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241211225219-79336bf6e886 h1:+7AijqdfRXdDc3zvj02Alqsk6Qd3owvlqPYQN1Hc1ME= +github.com/Layr-Labs/eigensdk-go v0.1.14-0.20241211225219-79336bf6e886/go.mod h1:aYdNURUhaqeYOS+Cq12TfSdPbjFfiLaHkxPdR4Exq/s= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= diff --git a/pkg/internal/common/flags/general.go b/pkg/internal/common/flags/general.go index 2ae9378c..4077c1a5 100644 --- a/pkg/internal/common/flags/general.go +++ b/pkg/internal/common/flags/general.go @@ -91,6 +91,13 @@ var ( Value: 3600, } + OperatorAddressFlag = cli.StringFlag{ + Name: "operator-address", + Aliases: []string{"oa", "operator"}, + Usage: "Operator address", + EnvVars: []string{"OPERATOR_ADDRESS"}, + } + BatchClaimFile = cli.StringFlag{ Name: "batch-claim-file", Aliases: []string{"bcf"}, diff --git a/pkg/operator.go b/pkg/operator.go index f3ca2ce4..7458852f 100644 --- a/pkg/operator.go +++ b/pkg/operator.go @@ -18,6 +18,8 @@ func OperatorCmd(p utils.Prompter) *cli.Command { operator.UpdateCmd(p), operator.UpdateMetadataURICmd(p), operator.GetApprovalCmd(p), + operator.SetOperatorSplitCmd(p), + operator.GetOperatorSplitCmd(p), }, } diff --git a/pkg/operator/get_operator_split.go b/pkg/operator/get_operator_split.go new file mode 100644 index 00000000..b9571474 --- /dev/null +++ b/pkg/operator/get_operator_split.go @@ -0,0 +1,124 @@ +package operator + +import ( + "sort" + + "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/operator/split" + "github.com/Layr-Labs/eigenlayer-cli/pkg/rewards" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + 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" +) + +func GetOperatorSplitCmd(p utils.Prompter) *cli.Command { + var operatorSplitCmd = &cli.Command{ + Name: "get-rewards-split", + Usage: "Get operator rewards split", + Action: func(cCtx *cli.Context) error { + return GetOperatorSplit(cCtx) + }, + After: telemetry.AfterRunAction(), + Flags: getGetOperatorSplitFlags(), + } + + return operatorSplitCmd +} + +func getGetOperatorSplitFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.ETHRpcUrlFlag, + &flags.OperatorAddressFlag, + &split.OperatorSplitFlag, + &rewards.RewardsCoordinatorAddressFlag, + &split.AVSAddressFlag, + } + + sort.Sort(cli.FlagsByName(baseFlags)) + return baseFlags +} + +func GetOperatorSplit(cCtx *cli.Context) error { + ctx := cCtx.Context + logger := common.GetLogger(cCtx) + + config, err := readAndValidateGetOperatorSplitConfig(cCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate operator split config", err) + } + + cCtx.App.Metadata["network"] = config.ChainID.String() + + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return eigenSdkUtils.WrapError("failed to create new eth client", err) + } + + elReader, err := elcontracts.NewReaderFromConfig( + elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, + ethClient, + logger, + ) + + if err != nil { + return eigenSdkUtils.WrapError("failed to get EL writer", err) + } + + logger.Infof("Getting operator split...") + + split, err := elReader.GetOperatorAVSSplit(ctx, config.OperatorAddress, config.AVSAddress) + + if err != nil { + return eigenSdkUtils.WrapError("failed to get operator split", err) + } + + logger.Infof("Operator split is %d", split) + + return nil +} + +func readAndValidateGetOperatorSplitConfig( + cCtx *cli.Context, + logger logging.Logger, +) (*split.GetOperatorAVSSplitConfig, error) { + network := cCtx.String(flags.NetworkFlag.Name) + rpcUrl := cCtx.String(flags.ETHRpcUrlFlag.Name) + + rewardsCoordinatorAddress := cCtx.String(rewards.RewardsCoordinatorAddressFlag.Name) + + var err error + if common.IsEmptyString(rewardsCoordinatorAddress) { + rewardsCoordinatorAddress, err = common.GetRewardCoordinatorAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + logger.Debugf("Using Rewards Coordinator address: %s", rewardsCoordinatorAddress) + + operatorAddress := gethcommon.HexToAddress(cCtx.String(flags.OperatorAddressFlag.Name)) + logger.Infof("Using operator address: %s", operatorAddress.String()) + + avsAddress := gethcommon.HexToAddress(cCtx.String(split.AVSAddressFlag.Name)) + logger.Infof("Using AVS address: %s", avsAddress.String()) + + chainID := utils.NetworkNameToChainId(network) + logger.Debugf("Using chain ID: %s", chainID.String()) + + return &split.GetOperatorAVSSplitConfig{ + Network: network, + RPCUrl: rpcUrl, + RewardsCoordinatorAddress: gethcommon.HexToAddress(rewardsCoordinatorAddress), + ChainID: chainID, + OperatorAddress: operatorAddress, + AVSAddress: avsAddress, + }, nil +} diff --git a/pkg/operator/set_operator_split.go b/pkg/operator/set_operator_split.go new file mode 100644 index 00000000..3c6c8460 --- /dev/null +++ b/pkg/operator/set_operator_split.go @@ -0,0 +1,202 @@ +package operator + +import ( + "fmt" + "sort" + + "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/operator/split" + "github.com/Layr-Labs/eigenlayer-cli/pkg/rewards" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/logging" + 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" +) + +func SetOperatorSplitCmd(p utils.Prompter) *cli.Command { + var operatorSplitCmd = &cli.Command{ + Name: "set-rewards-split", + Usage: "Set operator rewards split", + Action: func(cCtx *cli.Context) error { + return SetOperatorSplit(cCtx, p) + }, + After: telemetry.AfterRunAction(), + Flags: getSetOperatorSplitFlags(), + } + + return operatorSplitCmd +} + +func SetOperatorSplit(cCtx *cli.Context, p utils.Prompter) error { + ctx := cCtx.Context + logger := common.GetLogger(cCtx) + + config, err := readAndValidateSetOperatorSplitConfig(cCtx, logger) + if err != nil { + return eigenSdkUtils.WrapError("failed to read and validate operator split config", err) + } + + cCtx.App.Metadata["network"] = config.ChainID.String() + + ethClient, err := ethclient.Dial(config.RPCUrl) + if err != nil { + return eigenSdkUtils.WrapError("failed to create new eth client", err) + } + + if config.Broadcast { + + eLWriter, err := common.GetELWriter( + config.OperatorAddress, + config.SignerConfig, + ethClient, + elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, + p, + config.ChainID, + logger, + ) + + if err != nil { + return eigenSdkUtils.WrapError("failed to get EL writer", err) + } + + logger.Infof("Broadcasting set operator transaction...") + + receipt, err := eLWriter.SetOperatorAVSSplit(ctx, config.OperatorAddress, config.AVSAddress, config.Split, true) + + if err != nil { + return eigenSdkUtils.WrapError("failed to process claim", err) + } + + logger.Infof("Set operator transaction submitted successfully") + common.PrintTransactionInfo(receipt.TxHash.String(), config.ChainID) + } else { + noSendTxOpts := common.GetNoSendTxOpts(config.OperatorAddress) + _, _, contractBindings, err := elcontracts.BuildClients(elcontracts.Config{ + RewardsCoordinatorAddress: config.RewardsCoordinatorAddress, + }, ethClient, nil, logger, nil) + if err != nil { + return err + } + + code, err := ethClient.CodeAt(ctx, config.OperatorAddress, nil) + if err != nil { + return eigenSdkUtils.WrapError("failed to get code at address", err) + } + if len(code) > 0 { + // Operator is a smart contract + noSendTxOpts.GasLimit = 150_000 + } + + unsignedTx, err := contractBindings.RewardsCoordinator.SetOperatorAVSSplit(noSendTxOpts, config.OperatorAddress, config.AVSAddress, config.Split) + + if err != nil { + return eigenSdkUtils.WrapError("failed to create unsigned tx", err) + } + if config.OutputType == string(common.OutputType_Calldata) { + 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 { + logger.Infof("This transaction would set the operator split to %d", config.Split) + } + + if !config.IsSilent { + txFeeDetails := common.GetTxFeeDetails(unsignedTx) + fmt.Println() + txFeeDetails.Print() + + fmt.Println("To broadcast the operator set split, use the --broadcast flag") + } + } + return nil +} + +func getSetOperatorSplitFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &flags.NetworkFlag, + &flags.ETHRpcUrlFlag, + &flags.OperatorAddressFlag, + &split.OperatorSplitFlag, + &rewards.RewardsCoordinatorAddressFlag, + &split.AVSAddressFlag, + &flags.BroadcastFlag, + &flags.OutputTypeFlag, + &flags.OutputFileFlag, + &flags.SilentFlag, + } + + allFlags := append(baseFlags, flags.GetSignerFlags()...) + sort.Sort(cli.FlagsByName(allFlags)) + return allFlags +} + +func readAndValidateSetOperatorSplitConfig( + cCtx *cli.Context, + logger logging.Logger, +) (*split.SetOperatorAVSSplitConfig, error) { + network := cCtx.String(flags.NetworkFlag.Name) + rpcUrl := cCtx.String(flags.ETHRpcUrlFlag.Name) + opSplit := cCtx.Int(split.OperatorSplitFlag.Name) + broadcast := cCtx.Bool(flags.BroadcastFlag.Name) + outputType := cCtx.String(flags.OutputTypeFlag.Name) + outputFile := cCtx.String(flags.OutputFileFlag.Name) + isSilent := cCtx.Bool(flags.SilentFlag.Name) + + rewardsCoordinatorAddress := cCtx.String(rewards.RewardsCoordinatorAddressFlag.Name) + + var err error + if common.IsEmptyString(rewardsCoordinatorAddress) { + rewardsCoordinatorAddress, err = common.GetRewardCoordinatorAddress(utils.NetworkNameToChainId(network)) + if err != nil { + return nil, err + } + } + logger.Debugf("Using Rewards Coordinator address: %s", rewardsCoordinatorAddress) + + operatorAddress := gethcommon.HexToAddress(cCtx.String(flags.OperatorAddressFlag.Name)) + logger.Infof("Using operator address: %s", operatorAddress.String()) + + avsAddress := gethcommon.HexToAddress(cCtx.String(split.AVSAddressFlag.Name)) + logger.Infof("Using AVS address: %s", avsAddress.String()) + + chainID := utils.NetworkNameToChainId(network) + logger.Debugf("Using chain ID: %s", chainID.String()) + + // Get SignerConfig + signerConfig, err := common.GetSignerConfig(cCtx, 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 signer config: %s", err) + } + + return &split.SetOperatorAVSSplitConfig{ + Network: network, + RPCUrl: rpcUrl, + RewardsCoordinatorAddress: gethcommon.HexToAddress(rewardsCoordinatorAddress), + ChainID: chainID, + SignerConfig: signerConfig, + OperatorAddress: operatorAddress, + AVSAddress: avsAddress, + Split: uint16(opSplit), + Broadcast: broadcast, + OutputType: outputType, + OutputFile: outputFile, + IsSilent: isSilent, + }, nil +} diff --git a/pkg/operator/split/flags.go b/pkg/operator/split/flags.go new file mode 100644 index 00000000..c8ec6bcf --- /dev/null +++ b/pkg/operator/split/flags.go @@ -0,0 +1,20 @@ +package split + +import "github.com/urfave/cli/v2" + +var ( + OperatorSplitFlag = cli.IntFlag{ + Name: "operator-split", + Aliases: []string{"os"}, + Usage: "Split for the operator in bips (e.g. 1000 = 10%)", + Required: false, + EnvVars: []string{"OPERATOR_SPLIT"}, + } + + AVSAddressFlag = cli.StringFlag{ + Name: "avs-address", + Aliases: []string{"aa"}, + Usage: "AVS address to set operator split", + EnvVars: []string{"AVS_ADDRESS"}, + } +) diff --git a/pkg/operator/split/types.go b/pkg/operator/split/types.go new file mode 100644 index 00000000..fd98bc39 --- /dev/null +++ b/pkg/operator/split/types.go @@ -0,0 +1,33 @@ +package split + +import ( + "math/big" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/types" + gethcommon "github.com/ethereum/go-ethereum/common" +) + +type SetOperatorAVSSplitConfig struct { + Network string + RPCUrl string + RewardsCoordinatorAddress gethcommon.Address + ChainID *big.Int + SignerConfig *types.SignerConfig + Broadcast bool + OperatorAddress gethcommon.Address + AVSAddress gethcommon.Address + Split uint16 + OutputType string + OutputFile string + IsSilent bool +} + +type GetOperatorAVSSplitConfig struct { + Network string + RPCUrl string + RewardsCoordinatorAddress gethcommon.Address + ChainID *big.Int + + OperatorAddress gethcommon.Address + AVSAddress gethcommon.Address +}