diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml deleted file mode 100644 index 559db094..00000000 --- a/.github/workflows/cli_release.yml +++ /dev/null @@ -1,45 +0,0 @@ -# .github/workflows/release.yml -name: Release CLI Binaries - -on: - pull_request: - push: - # run only against tags - tags: - - "*" - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: stable - - name: Run GoReleaser (tagged release) - uses: goreleaser/goreleaser-action@v6 - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }} - with: - distribution: goreleaser - version: "~> v2" - args: release --clean - workdir: cli - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run GoReleaser (branch) - uses: goreleaser/goreleaser-action@v6 - if: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }} - with: - distribution: goreleaser - version: "~> v2" - args: release --snapshot - workdir: cli - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml deleted file mode 100644 index 41500408..00000000 --- a/.github/workflows/go_test.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Run Go Tests - -on: - push: - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21' - - - name: Install dependencies - run: | - go mod download - sudo apt-get install -y unzip - - - name: Download blockchain data - run: | - curl -o data/deneb_holesky_beacon_state_2227472.ssz.zip https://dviu8zszosyat.cloudfront.net/deneb_holesky_beacon_state_2227472.ssz.zip - (cd data && unzip deneb_holesky_beacon_state_2227472.ssz.zip) - - - name: Run tests - run: | - go test -v ./... diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 7670221c..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Lint - -on: - push: - branches: - - main - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Go 1.22 - uses: actions/setup-go@v1 - env: - GOPATH: /home/runner/.go - with: - go-version: 1.22.4 - - - name: Install dependencies - env: - GOPATH: /home/runner/.go - run: | - mkdir /home/runner/.go - make setup - go env - ls -lar $GOPATH - - - name: Run Lint - env: - GOPATH: /home/runner/.go - run: /home/runner/.go/bin/golangci-lint run ./cli diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 00000000..a1101859 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,97 @@ +# .github/workflows/release.yml +name: Build, test, release + +on: + pull_request: + push: + # run only against tags + tags: + - "*" + +permissions: + contents: write + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go 1.22 + uses: actions/setup-go@v1 + env: + GOPATH: /home/runner/.go + with: + go-version: 1.22.4 + + - name: Install dependencies + env: + GOPATH: /home/runner/.go + run: | + mkdir /home/runner/.go + make setup + go env + ls -lar $GOPATH + + - name: Run Lint + env: + GOPATH: /home/runner/.go + run: /home/runner/.go/bin/golangci-lint run ./cli + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Install dependencies + run: | + go mod download + sudo apt-get install -y unzip + + - name: Download blockchain data + run: | + curl -o data/deneb_holesky_beacon_state_2227472.ssz.zip https://dviu8zszosyat.cloudfront.net/deneb_holesky_beacon_state_2227472.ssz.zip + (cd data && unzip deneb_holesky_beacon_state_2227472.ssz.zip) + + - name: Run tests + run: | + go test -v ./... + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + - name: Run GoReleaser (tagged release) + uses: goreleaser/goreleaser-action@v6 + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }} + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + workdir: cli + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run GoReleaser (branch) + uses: goreleaser/goreleaser-action@v6 + if: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }} + with: + distribution: goreleaser + version: "~> v2" + args: release --snapshot + workdir: cli + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 48b24d48..9463c636 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,7 +12,7 @@ linters-settings: - performance - style govet: - check-shadowing: true + shadow: true enable: - fieldalignment nolintlint: @@ -26,12 +26,11 @@ linters: - dogsled - dupl - errcheck - - exportloopref + - copyloopvar - exhaustive - goconst - gocritic - gofmt - - gomnd - gocyclo - gosec - gosimple diff --git a/cli/commands/completeAllWithdrawals.go b/cli/commands/completeAllWithdrawals.go new file mode 100644 index 00000000..9399f0dc --- /dev/null +++ b/cli/commands/completeAllWithdrawals.go @@ -0,0 +1,172 @@ +package commands + +import ( + "context" + "fmt" + "math/big" + + "github.com/pkg/errors" + lo "github.com/samber/lo" + + "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/EigenPod" + "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IDelegationManager" + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/fatih/color" +) + +type TCompleteWithdrawalArgs struct { + EthNode string + EigenPod string + Sender string + EstimateGas bool +} + +func DelegationManager(chainId *big.Int) common.Address { + data := map[uint64]string{ + // TODO(zeus) - make this runnable via zeus. + 1: "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A", // mainnet + 17000: "0x75dfE5B44C2E530568001400D3f704bC8AE350CC", // holesky preprod + } + contract, ok := data[chainId.Uint64()] + if !ok { + panic("no delegation manager found for chain") + } + addr := common.HexToAddress(contract) + return addr +} + +func CompleteAllWithdrawalsCommand(args TCompleteWithdrawalArgs) error { + ctx := context.Background() + + isSimulation := args.EstimateGas + + eth, err := ethclient.DialContext(ctx, args.EthNode) + core.PanicOnError("failed to reach eth node", err) + + chainId, err := eth.ChainID(ctx) + core.PanicOnError("failed to load chainId", err) + + acc, err := core.PrepareAccount(&args.Sender, chainId, isSimulation) + core.PanicOnError("failed to parse private key", err) + + curBlockNumber, err := eth.BlockNumber(ctx) + core.PanicOnError("failed to load current block number", err) + + pod, err := EigenPod.NewEigenPod(common.HexToAddress(args.EigenPod), eth) + core.PanicOnError("failed to reach eigenpod", err) + + reg, err := pod.WithdrawableRestakedExecutionLayerGwei(nil) + core.PanicOnError("failed to fetch REG", err) + rew := core.GweiToWei(new(big.Float).SetUint64(reg)) + + podOwner, err := pod.PodOwner(nil) + core.PanicOnError("failed to read podOwner", err) + + delegationManager, err := IDelegationManager.NewIDelegationManager(DelegationManager(chainId), eth) + core.PanicOnError("failed to reach delegation manager", err) + + minDelay, err := delegationManager.MinWithdrawalDelayBlocks(nil) + core.PanicOnError("failed to read MinWithdrawalDelayBlocks", err) + + queuedWithdrawals, err := delegationManager.GetQueuedWithdrawals(nil, podOwner) + core.PanicOnError("failed to read queuedWithdrawals", err) + + eligibleWithdrawals := lo.Map(queuedWithdrawals.Withdrawals, func(withdrawal IDelegationManager.IDelegationManagerTypesWithdrawal, index int) *IDelegationManager.IDelegationManagerTypesWithdrawal { + isBeaconWithdrawal := len(withdrawal.Strategies) == 1 && withdrawal.Strategies[0].Cmp(core.BeaconStrategy()) == 0 + isCompletable := curBlockNumber > uint64(withdrawal.StartBlock+minDelay) + if isBeaconWithdrawal && isCompletable { + return &withdrawal + } + return nil + }) + + readyWithdrawalCount := len(lo.Filter(eligibleWithdrawals, func(withdrawal *IDelegationManager.IDelegationManagerTypesWithdrawal, index int) bool { + return withdrawal != nil + })) + + if readyWithdrawalCount == 0 { + fmt.Printf("Your pod has no eligible withdrawals.\n") + return nil + } + + var runningSumWei *big.Float = new(big.Float) + + affordedWithdrawals := lo.Map(eligibleWithdrawals, func(withdrawal *IDelegationManager.IDelegationManagerTypesWithdrawal, index int) *IDelegationManager.IDelegationManagerTypesWithdrawal { + if withdrawal == nil { + return nil + } + withdrawalShares := queuedWithdrawals.Shares[index][0] + // if REW > runningSumWei + withdrawalShares, we can complete with withdrawal. + if rew.Cmp( + new(big.Float).Add( + runningSumWei, + new(big.Float).SetInt(withdrawalShares), + ), + ) > 0 { + runningSumWei = new(big.Float).Add(runningSumWei, new(big.Float).SetInt(withdrawalShares)) + return withdrawal + } + return nil + }) + + affordedWithdrawals = lo.Filter(affordedWithdrawals, func(withdrawal *IDelegationManager.IDelegationManagerTypesWithdrawal, index int) bool { + return withdrawal != nil + }) + + if len(affordedWithdrawals) == 0 && readyWithdrawalCount > 0 { + color.Yellow("WARN: Your pod has %d withdrawal(s) available, but your pod does not have enough funding to proceed.\n", readyWithdrawalCount) + color.Yellow("Consider checkpointing to claim beacon rewards, or depositing ETH and checkpointing to complete these withdrawals.\n\n") + return errors.New("Insufficient funds") + } + + if len(affordedWithdrawals) != readyWithdrawalCount { + color.Yellow("WARN: Your pod has %d withdrawal(s) available, but you only have enough balance to satisfy %d of them.\n", readyWithdrawalCount, len(affordedWithdrawals)) + color.Yellow("Consider checkpointing to claim beacon rewards, or depositing ETH and checkpointing to complete these withdrawals.\n\n") + } + + fmt.Printf("Your podOwner(%s) has %d withdrawal(s) that can be completed right now.\n", podOwner.Hex(), len(affordedWithdrawals)) + runningSumWeiInt, _ := runningSumWei.Int(nil) + fmt.Printf("Total ETH on all withdrawals: %sETH\n", core.GweiToEther(core.WeiToGwei(runningSumWeiInt)).String()) + + if !isSimulation { + core.PanicIfNoConsent("Would you like to continue?") + } else { + color.Yellow("THIS IS A SIMULATION. No transaction will be recorded onchain.\n") + } + + withdrawals := lo.Map(affordedWithdrawals, func(w *IDelegationManager.IDelegationManagerTypesWithdrawal, i int) IDelegationManager.IDelegationManagerTypesWithdrawal { + return *w + }) + + tokens := lo.Map(withdrawals, func(_ IDelegationManager.IDelegationManagerTypesWithdrawal, _ int) []common.Address { + return []common.Address{common.BigToAddress(big.NewInt(0))} + }) + + receiveAsTokens := lo.Map(withdrawals, func(_ IDelegationManager.IDelegationManagerTypesWithdrawal, _ int) bool { + return true + }) + + txn, err := delegationManager.CompleteQueuedWithdrawals(acc.TransactionOptions, withdrawals, tokens, receiveAsTokens) + core.PanicOnError("CompleteQueuedWithdrawals failed.", err) + + if !isSimulation { + _, err := bind.WaitMined(ctx, eth, txn) + core.PanicOnError("waitMined failed", err) + + color.Green("%s\n", txn.Hash().Hex()) + } else { + printAsJSON(Transaction{ + Type: "complete-withdrawals", + To: txn.To().Hex(), + CallData: common.Bytes2Hex(txn.Data()), + GasEstimateGwei: func() *uint64 { + gas := txn.Gas() + return &gas + }(), + }) + } + return nil +} diff --git a/cli/commands/queueWithdrawal.go b/cli/commands/queueWithdrawal.go new file mode 100644 index 00000000..eeaed4f2 --- /dev/null +++ b/cli/commands/queueWithdrawal.go @@ -0,0 +1,123 @@ +package commands + +import ( + "context" + "errors" + "fmt" + "math/big" + + "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/EigenPod" + "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IDelegationManager" + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/fatih/color" +) + +type TQueueWithdrawallArgs struct { + EthNode string + EigenPod string + Sender string + EstimateGas bool + AmountWei uint64 +} + +func QueueWithdrawalCommand(args TQueueWithdrawallArgs) error { + ctx := context.Background() + + isSimulation := args.EstimateGas + + eth, err := ethclient.DialContext(ctx, args.EthNode) + core.PanicOnError("failed to reach eth node", err) + + chainId, err := eth.ChainID(ctx) + core.PanicOnError("failed to load chainId", err) + + acc, err := core.PrepareAccount(&args.Sender, chainId, args.EstimateGas) + core.PanicOnError("failed to parse private key", err) + + dm, err := IDelegationManager.NewIDelegationManager(DelegationManager(chainId), eth) + core.PanicOnError("failed to reach delegation manager", err) + + pod, err := EigenPod.NewEigenPod(common.HexToAddress(args.EigenPod), eth) + core.PanicOnError("failed to reach eigenpod", err) + + _reg, err := pod.WithdrawableRestakedExecutionLayerGwei(nil) + core.PanicOnError("failed to load REG", err) + + // [withdrawable]RestakedExecutionlayerWei + rew := core.GweiToWei(new(big.Float).SetUint64(_reg)) + if args.AmountWei > 0 && new(big.Float).SetUint64(args.AmountWei).Cmp(rew) > 0 { + return errors.New("invalid --amountWei. must be in the range (0, pod.withdrawableRestakedExecutionLayerGwei() as wei]") + } + + podOwner, err := pod.PodOwner(nil) + core.PanicOnError("failed to read podOwner", err) + + reg := new(big.Int).SetUint64(_reg) + + // TODO: maximumWithdrawalSizeGwei = reg - gwei(sumQueuedWithdrawals) + maximumWithdrawalSizeWei := core.IGweiToWei(reg) + var requestedWithdrawalSizeWei *big.Int + if args.AmountWei == 0 { + // default to the number of withdrawable shares in the beacon strategy + requestedWithdrawalSizeWei = maximumWithdrawalSizeWei + } else { + // if it is specified, we withdraw the specific amount. + requestedWithdrawalSizeWei = new(big.Int).SetUint64(args.AmountWei) + } + + if requestedWithdrawalSizeWei.Cmp(maximumWithdrawalSizeWei) > 0 { + color.Red( + "Error: the amount to withdraw from the native ETH strategy (%sETH) is larger than the total withdrawable amount from the pod (%sETH). Will attempt a smaller withdrawal.\n", + core.IweiToEther(requestedWithdrawalSizeWei).String(), + core.IweiToEther(maximumWithdrawalSizeWei).String(), + ) + return errors.New("requested to withdraw too many shares") + } + + _depositShares, err := dm.ConvertToDepositShares(nil, podOwner, []common.Address{core.BeaconStrategy()}, []*big.Int{requestedWithdrawalSizeWei}) + core.PanicOnError("failed to compute deposit shares", err) + depositShares := _depositShares[0] + + minWithdrawalDelay, err := dm.MinWithdrawalDelayBlocks(nil) + core.PanicOnError("failed to load minWithdrawalDelay", err) + + curBlock, err := eth.BlockNumber(ctx) + core.PanicOnError("failed to load current block number", err) + + requestedWithdrawalSizeEth := core.GweiToEther(core.WeiToGwei(requestedWithdrawalSizeWei)) + color.Blue("Withdrawing: %sETH.\n", requestedWithdrawalSizeEth.String()) + color.Yellow("NOTE: If you were or become slashed on EigenLayer during the withdrawal period, the total amount received will be less any slashed amount.\n") + + if !isSimulation { + core.PanicIfNoConsent(fmt.Sprintf("Would you like to queue a withdrawal %sETH from the Native ETH strategy? This will be withdrawable after approximately block #%d (current block: %d)\n", requestedWithdrawalSizeEth.String(), curBlock+uint64(minWithdrawalDelay), curBlock)) + } else { + fmt.Printf("THIS IS A SIMULATION. No transaction will be recorded onchain.\n") + } + txn, err := dm.QueueWithdrawals(acc.TransactionOptions, []IDelegationManager.IDelegationManagerTypesQueuedWithdrawalParams{ + { + Strategies: []common.Address{core.BeaconStrategy()}, + DepositShares: []*big.Int{depositShares}, + Withdrawer: podOwner, + }, + }) + core.PanicOnError("failed to queue withdrawal", err) + if !isSimulation { + txnReceipt, err := bind.WaitMined(ctx, eth, txn) + core.PanicOnError("failed to wait for txn", err) + color.Green("%s\n", txnReceipt.TxHash.Hex()) + } else { + printAsJSON(Transaction{ + Type: "queue-withdrawal", + To: txn.To().Hex(), + CallData: common.Bytes2Hex(txn.Data()), + GasEstimateGwei: func() *uint64 { + gas := txn.Gas() + return &gas + }(), + }) + } + return nil +} diff --git a/cli/commands/showWithdrawals.go b/cli/commands/showWithdrawals.go new file mode 100644 index 00000000..943b4faa --- /dev/null +++ b/cli/commands/showWithdrawals.go @@ -0,0 +1,75 @@ +package commands + +import ( + "context" + "math/big" + + "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/EigenPod" + "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/IDelegationManager" + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" + "github.com/ethereum/go-ethereum/common" +) + +type TShowWithdrawalArgs struct { + EthNode string + EigenPod string + Strategies common.Address +} + +func ShowWithdrawalsCommand(args TShowWithdrawalArgs) error { + ctx := context.Background() + eth, chainId, err := core.GetEthClient(ctx, args.EthNode) + core.PanicOnError("failed to reach eth and beacon node", err) + + curBlock, err := eth.BlockByNumber(ctx, nil) /* head */ + core.PanicOnError("failed to load curBlock", err) + + dm, err := IDelegationManager.NewIDelegationManager(DelegationManager(chainId), eth) + core.PanicOnError("failed to reach delegation manager", err) + + pod, err := EigenPod.NewEigenPod(common.HexToAddress(args.EigenPod), eth) + core.PanicOnError("failed to reach eigenpod manager", err) + + podOwner, err := pod.PodOwner(nil) + core.PanicOnError("failed to load podOwner", err) + + allWithdrawals, err := dm.GetQueuedWithdrawals(nil, podOwner) + core.PanicOnError("failed to get queued withdrawals", err) + + type TWithdrawalInfo struct { + Staker string + Strategy common.Address + CurrentBlock uint64 + AvailableAfterBlock *big.Int + Ready bool + TotalAmountETH *big.Float + } + + minDelay, err := dm.MinWithdrawalDelayBlocks(nil) + core.PanicOnError("failed to get minWithdrawalDelay", err) + + withdrawalInfo := []TWithdrawalInfo{} + + for i, shares := range allWithdrawals.Shares { + withdrawal := allWithdrawals.Withdrawals[i] + + // this cli is only for withdrawals of beaconstrategy for a single strategy + if withdrawal.Strategies[0].Cmp(core.BeaconStrategy()) != 0 || len(withdrawal.Strategies) != 1 { + continue + } + + targetBlock := new(big.Int).SetUint64(uint64(allWithdrawals.Withdrawals[i].StartBlock + minDelay)) + + withdrawalInfo = append(withdrawalInfo, TWithdrawalInfo{ + TotalAmountETH: core.GweiToEther(core.WeiToGwei(shares[0])), + Strategy: allWithdrawals.Withdrawals[i].Strategies[0], + Staker: allWithdrawals.Withdrawals[i].Staker.Hex(), + CurrentBlock: curBlock.NumberU64(), + AvailableAfterBlock: targetBlock, + Ready: targetBlock.Uint64() < curBlock.NumberU64(), + }) + } + + printAsJSON(withdrawalInfo) + return nil +} diff --git a/cli/core/findStalePods.go b/cli/core/findStalePods.go index 3317473a..af6c2be2 100644 --- a/cli/core/findStalePods.go +++ b/cli/core/findStalePods.go @@ -42,7 +42,7 @@ func executionWithdrawalAddress(withdrawalCredentials []byte) *string { return &addr } -func validEigenpodsOnly(candidateAddresses []common.Address, mc *multicall.MulticallClient, chainId uint64, eth *ethclient.Client) ([]common.Address, error) { +func validEigenpodsOnly(candidateAddresses []common.Address, mc *multicall.MulticallClient, chainId uint64) ([]common.Address, error) { EigenPodAbi, err := abi.JSON(strings.NewReader(EigenPod.EigenPodABI)) if err != nil { return nil, fmt.Errorf("failed to load eigenpod abi: %s", err) @@ -54,7 +54,7 @@ func validEigenpodsOnly(candidateAddresses []common.Address, mc *multicall.Multi podManagerAddress, ok := PodManagerContracts()[chainId] if !ok { - return nil, fmt.Errorf("Unsupported chainId: %d", chainId) + return nil, fmt.Errorf("unsupported chainId: %d", chainId) } ////// step 1: cast all addresses to EigenPod, and attempt to read the pod owner. @@ -117,6 +117,10 @@ func validEigenpodsOnly(candidateAddresses []common.Address, mc *multicall.Multi } authoritativeOwnerToPod, err := multicall.DoMany(mc, authoritativeOwnerToPodCalls...) + if err != nil { + return nil, err + } + nullAddress := common.BigToAddress(big.NewInt(0)) ////// step 3: the valid eigenrestpods are the ones where authoritativeOwnerToPod[i] == candidateAddresses[i]. @@ -186,7 +190,7 @@ func ComputeBalanceDeviationSync(ctx context.Context, eth *ethclient.Client, sta PanicOnError("failed to load pod owner", err) activeShares, err := delegationManager.GetWithdrawableShares(nil, podOwner, []common.Address{ - common.HexToAddress(NATIVE_ETH_STRATEGY), + BeaconStrategy(), }) PanicOnError("failed to load owner shares", err) @@ -196,7 +200,7 @@ func ComputeBalanceDeviationSync(ctx context.Context, eth *ethclient.Client, sta for i, withdrawal := range withdrawalInfo.Withdrawals { for j, strategy := range withdrawal.Strategies { - if strategy.Cmp(common.HexToAddress(NATIVE_ETH_STRATEGY)) == 0 { + if strategy.Cmp(BeaconStrategy()) == 0 { sharesPendingWithdrawal = new(big.Int).Add(sharesPendingWithdrawal, withdrawalInfo.Shares[i][j]) } } @@ -274,8 +278,10 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri ) // fmt.Printf("Checking %d slashed withdrawal addresses for eigenpod status\n", len(allSlashedWithdrawalAddresses)) - - slashedEigenpods, err := validEigenpodsOnly(allSlashedWithdrawalAddresses, mc, chainId.Uint64(), eth) + slashedEigenpods, err := validEigenpodsOnly(allSlashedWithdrawalAddresses, mc, chainId.Uint64()) + if err != nil { + return nil, err + } if len(slashedEigenpods) == 0 { log.Println("No eigenpods were slashed.") diff --git a/cli/core/status.go b/cli/core/status.go index 5a848e9c..b6fe1b86 100644 --- a/cli/core/status.go +++ b/cli/core/status.go @@ -10,7 +10,6 @@ import ( "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/EigenPodManager" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/ethereum/go-ethereum/common" gethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) @@ -31,7 +30,9 @@ type Validator struct { CurrentBalance uint64 } -const NATIVE_ETH_STRATEGY = "0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0" +func BeaconStrategy() gethCommon.Address { + return gethCommon.HexToAddress("0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0") +} type EigenpodStatus struct { Validators map[string]Validator @@ -155,8 +156,8 @@ func GetStatus(ctx context.Context, eigenpodAddress string, eth *ethclient.Clien delegationManager, err := DelegationManager.NewDelegationManager(delegationManagerAddress, eth) PanicOnError("failed to reach delegationManager", err) - shares, err := delegationManager.GetWithdrawableShares(nil, eigenPodOwner, []common.Address{ - common.HexToAddress(NATIVE_ETH_STRATEGY), + shares, err := delegationManager.GetWithdrawableShares(nil, eigenPodOwner, []gethCommon.Address{ + BeaconStrategy(), }) PanicOnError("failed to load owner shares", err) diff --git a/cli/core/utils.go b/cli/core/utils.go index 0cf0cc63..34a55930 100644 --- a/cli/core/utils.go +++ b/cli/core/utils.go @@ -430,19 +430,27 @@ func ForkVersions() map[uint64]string { } } -func GetClients(ctx context.Context, node, beaconNodeUri string, enableLogs bool) (*ethclient.Client, BeaconClient, *big.Int, error) { +func GetEthClient(ctx context.Context, node string) (*ethclient.Client, *big.Int, error) { eth, err := ethclient.Dial(node) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to reach eth --node: %w", err) + return nil, nil, fmt.Errorf("failed to reach eth --node: %w", err) } chainId, err := eth.ChainID(ctx) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to fetch chain id: %w", err) + return nil, nil, fmt.Errorf("failed to fetch chain id: %w", err) } if chainId == nil || (chainId.Int64() != 17000 && chainId.Int64() != 1) { - return nil, nil, nil, errors.New("this tool only supports the Holesky and Mainnet Ethereum Networks") + return nil, nil, errors.New("this tool only supports the Holesky and Mainnet Ethereum Networks") + } + return eth, chainId, nil +} + +func GetClients(ctx context.Context, node, beaconNodeUri string, enableLogs bool) (*ethclient.Client, BeaconClient, *big.Int, error) { + eth, chainId, err := GetEthClient(ctx, node) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to reach eth --node: %w", err) } beaconClient, err := GetBeaconClient(beaconNodeUri, enableLogs) diff --git a/cli/flags.go b/cli/flags.go index 31e02ef0..27335e2b 100644 --- a/cli/flags.go +++ b/cli/flags.go @@ -12,6 +12,15 @@ var PodAddressFlag = &cli.StringFlag{ Destination: &eigenpodAddress, } +var PodOwnerFlag = &cli.StringFlag{ + Name: "podOwner", + Aliases: []string{"p", "podOwner"}, + Value: "", + Usage: "[required] The onchain `address` of your eigenpod's owner (0x123123123123)", + Required: true, + Destination: &eigenpodOwner, +} + // Required for commands that need a beacon chain RPC var BeaconNodeFlag = &cli.StringFlag{ Name: "beaconNode", @@ -51,6 +60,14 @@ var EstimateGasFlag = &cli.BoolFlag{ Destination: &estimateGas, } +var AmountWeiFlag = &cli.Uint64Flag{ + Name: "amountWei", + Aliases: []string{}, + Value: 0, + Usage: "The amount, in Wei.", + Destination: &amountWei, +} + // Optional use for commands that support JSON output var PrintJSONFlag = &cli.BoolFlag{ Name: "json", diff --git a/cli/main.go b/cli/main.go index 99fed8b2..0dfb46e8 100644 --- a/cli/main.go +++ b/cli/main.go @@ -10,11 +10,12 @@ import ( ) // Destinations for values set by various flags -var eigenpodAddress, beacon, node, sender string +var eigenpodAddress, beacon, node, sender, eigenpodOwner string var useJSON = false var specificValidator uint64 = math.MaxUint64 var estimateGas = false var slashedValidatorIndex uint64 +var amountWei uint64 const DefaultHealthcheckTolerance = float64(5.0) @@ -196,6 +197,61 @@ func main() { }) }, }, + { + Name: "complete-all-withdrawals", + Args: true, + Usage: "Completes all withdrawals queued on the podOwner, for which Native ETH is the sole strategy in the withdrawal. Attempts to execute a group of withdrawals whose sum does not exceed Pod.withdrawableRestakedExecutionLayerGwei() in value.", + Flags: []cli.Flag{ + ExecNodeFlag, + PodAddressFlag, + SenderPkFlag, + EstimateGasFlag, + }, + Action: func(_ *cli.Context) error { + return commands.CompleteAllWithdrawalsCommand(commands.TCompleteWithdrawalArgs{ + EthNode: node, + EigenPod: eigenpodAddress, + Sender: sender, + EstimateGas: estimateGas, + }) + }, + }, + { + Name: "queue-withdrawal", + Args: true, + Usage: "Queues a withdrawal for shares associated with the native ETH strategy. Queues a withdrawal whose size does not exceed Pod.withdrawableRestakedExecutionLayerGwei() in value.", + Flags: []cli.Flag{ + ExecNodeFlag, + PodAddressFlag, + SenderPkFlag, + EstimateGasFlag, + AmountWeiFlag, + }, + Action: func(_ *cli.Context) error { + return commands.QueueWithdrawalCommand(commands.TQueueWithdrawallArgs{ + EthNode: node, + EigenPod: eigenpodAddress, + Sender: sender, + EstimateGas: estimateGas, + AmountWei: amountWei, + }) + }, + }, + { + Name: "show-withdrawals", + Args: true, + Usage: "Shows all pending withdrawals for the podOwner.", + Flags: []cli.Flag{ + ExecNodeFlag, + PodAddressFlag, + }, + Action: func(_ *cli.Context) error { + return commands.ShowWithdrawalsCommand(commands.TShowWithdrawalArgs{ + EthNode: node, + EigenPod: eigenpodAddress, + }) + }, + }, }, Flags: []cli.Flag{ &cli.BoolFlag{ diff --git a/go.mod b/go.mod index e737d3e5..560241c2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.0 toolchain go1.22.4 require ( - github.com/Layr-Labs/eigenlayer-contracts v0.4.3-mainnet-rewards-foundation-incentives.0.20241216232148-75428be65f85 + github.com/Layr-Labs/eigenlayer-contracts v0.4.3-mainnet-rewards-foundation-incentives.0.20241218180135-1856b33e74ac github.com/attestantio/go-eth2-client v0.19.9 github.com/ethereum/go-ethereum v1.14.9 github.com/fatih/color v1.16.0 @@ -15,6 +15,7 @@ require ( github.com/minio/sha256-simd v1.0.1 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.29.1 + github.com/samber/lo v1.47.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.1 ) @@ -71,6 +72,7 @@ require ( golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect diff --git a/go.sum b/go.sum index 941725d2..5c73e8d2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Layr-Labs/eigenlayer-contracts v0.4.3-mainnet-rewards-foundation-incentives.0.20241216232148-75428be65f85 h1:nyMTaafrT29KlAKFisfhVVCRU+MZucnnIgO3M8M6qe0= -github.com/Layr-Labs/eigenlayer-contracts v0.4.3-mainnet-rewards-foundation-incentives.0.20241216232148-75428be65f85/go.mod h1:Ie8YE3EQkTHqG6/tnUS0He7/UPMkXPo/3OFXwSy0iRo= +github.com/Layr-Labs/eigenlayer-contracts v0.4.3-mainnet-rewards-foundation-incentives.0.20241218180135-1856b33e74ac h1:YKNXU2HFHxdR7XyrUxeXyVWf2u5tMCwKA3/2YKT54Oo= +github.com/Layr-Labs/eigenlayer-contracts v0.4.3-mainnet-rewards-foundation-incentives.0.20241218180135-1856b33e74ac/go.mod h1:Ie8YE3EQkTHqG6/tnUS0He7/UPMkXPo/3OFXwSy0iRo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= @@ -189,6 +189,8 @@ github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA=