Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion bindings/megapool/megapool-contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ func (mp *megapoolV1) GetValidatorInfo(validatorId uint32, opts *bind.CallOpts)
return ValidatorInfo{}, fmt.Errorf("error creating calldata for getValidatorInfo: %w", err)
}

response, err := mp.Contract.Client.CallContract(context.Background(), ethereum.CallMsg{To: mp.Contract.Address, Data: callData}, nil)
var blockNumber *big.Int
if opts != nil && opts.BlockNumber != nil {
blockNumber = opts.BlockNumber
}
response, err := mp.Contract.Client.CallContract(context.Background(), ethereum.CallMsg{To: mp.Contract.Address, Data: callData}, blockNumber)
if err != nil {
return ValidatorInfo{}, fmt.Errorf("error calling getValidatorInfo: %w", err)
}
Expand Down
25 changes: 25 additions & 0 deletions bindings/network/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,31 @@ func ForceMegapoolExit(rp *rocketpool.RocketPool, megapoolAddress common.Address
return tx.Hash(), nil
}

// Estimate the gas to exit a list of megapool validators. The caller must fund
// getExitFee() * len(validatorIds) through opts.Value. Non-owner callers must
// satisfy the contract's projected deficit check.
func EstimateExitMegapoolValidatorsGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorIds []uint32, opts *bind.TransactOpts) (gaslimit.Limits, error) {
rocketNetworkExit, err := getRocketNetworkExit(rp, nil)
if err != nil {
return gaslimit.Limits{}, err
}
return rocketNetworkExit.GetTransactionGasInfo(opts, "exitMegapoolValidators", megapoolAddress, validatorIds)
}

// Exit a list of megapool validators through RocketNetworkExit. The caller
// supplies the total EIP-7002 fee in opts.Value.
func ExitMegapoolValidators(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorIds []uint32, opts *bind.TransactOpts) (common.Hash, error) {
rocketNetworkExit, err := getRocketNetworkExit(rp, nil)
if err != nil {
return common.Hash{}, err
}
tx, err := rocketNetworkExit.Transact(opts, "exitMegapoolValidators", megapoolAddress, validatorIds)
if err != nil {
return common.Hash{}, fmt.Errorf("error exiting validators for megapool %s: %w", megapoolAddress.Hex(), err)
}
return tx.Hash(), nil
}

// Get MinipoolExitRequested events emitted during the given block range
func GetMinipoolExitRequests(rp *rocketpool.RocketPool, intervalSize *big.Int, fromBlock *big.Int, toBlock *big.Int, opts *bind.CallOpts) ([]MinipoolExitRequest, error) {
rocketNetworkExit, err := getRocketNetworkExit(rp, opts)
Expand Down
13 changes: 13 additions & 0 deletions bindings/settings/protocol/megapool.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ const (
MegapoolPrestakeChallengePeriodPath string = "prestake.challenge.period"
)

// Get the deficit threshold in wei for permissionless megapool exits (Saturn 2).
func GetMegapoolExitDeficit(rp *rocketpool.RocketPool, opts *bind.CallOpts) (*big.Int, error) {
contract, err := getMegapoolSettingsContract(rp, opts)
if err != nil {
return nil, err
}
value := new(*big.Int)
if err := contract.Call(opts, value, "getExitDeficit"); err != nil {
return nil, fmt.Errorf("error getting megapool exit deficit: %w", err)
}
return *value, nil
}

// How long after an assignment a watcher must wait to dissolve a megapool validator
func GetMegapoolTimeBeforeDissolve(rp *rocketpool.RocketPool, opts *bind.CallOpts) (uint64, error) {
megapoolSettingsContract, err := getMegapoolSettingsContract(rp, opts)
Expand Down
27 changes: 27 additions & 0 deletions rocketpool-cli/megapool/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,33 @@ func RegisterCommands(app *cli.Command, name string, aliases []string) {
return dissolveValidator(validatorId, c.Bool("yes"))
},
},
{
Name: "exit-deficit",
Usage: "Force exits from another operator's megapool when its deficit permits (Saturn 2 only)",
UsageText: "rocketpool megapool exit-deficit megapool-address [options]",
Description: "Exits validators on another operator's megapool while its deficit still permits a permissionless exit. Exits the fewest validators that bring the deficit under the exit threshold when possible, and otherwise exits every eligible validator. Your node pays transaction gas and the EIP-7002 exit fees.",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Automatically confirm the exits and fees",
},
},
Action: func(ctx context.Context, c *cli.Command) error {

// Validate args
if err := cliutils.ValidateArgCount(c, 1); err != nil {
return err
}
address, err := cliutils.ValidateAddress("megapool address", c.Args().Get(0))
if err != nil {
return err
}

// Run
return exitDeficit(address, c.Bool("yes"))
},
},
{
Name: "exit-validator",
Aliases: []string{"t"},
Expand Down
86 changes: 86 additions & 0 deletions rocketpool-cli/megapool/exit-deficit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package megapool

import (
"fmt"
"math/big"
"strconv"
"strings"

"github.com/ethereum/go-ethereum/common"

cliutils "github.com/rocket-pool/smartnode/rocketpool-cli/cli"
"github.com/rocket-pool/smartnode/rocketpool-cli/cli/prompt"
"github.com/rocket-pool/smartnode/shared/math"
"github.com/rocket-pool/smartnode/shared/services/gas"
"github.com/rocket-pool/smartnode/shared/services/rocketpool"
)

func exitDeficit(address common.Address, yes bool) error {
rp, err := rocketpool.NewClient().WithReady()
if err != nil {
return err
}
defer rp.Close()

plan, err := rp.MegapoolDeficit(address)
if err != nil {
return err
}
if !plan.Saturn2Deployed {
fmt.Println(cliutils.Saturn2NotDeployedMessage)
return nil
}
formatETH := func(value *big.Int) string {
return new(big.Rat).SetFrac(value, big.NewInt(1e18)).FloatString(18)
}
fmt.Printf("Megapool: %s\n", address.Hex())
fmt.Printf("Current deficit: %s ETH\nDeficit required to permit an exit: %s ETH\n", formatETH(plan.Deficit), formatETH(plan.ExitDeficit))
if plan.ExistingExits > 0 {
fmt.Printf("Validators already exiting: %d\nDeficit after those exits: %s ETH\n", plan.ExistingExits, formatETH(plan.DeficitAfterPendingExits))
}
if !plan.CanExit {
fmt.Println(plan.Reason)
return nil
}
fmt.Printf("Validators to exit: %d\nProjected deficit after these exits: %s ETH\nValidator IDs: %v\n", len(plan.ValidatorIds), formatETH(plan.ProjectedDeficit), plan.ValidatorIds)
if plan.ProjectedDeficit.Cmp(plan.ExitDeficit) >= 0 {
fmt.Println("The deficit stays at or above the amount required to permit an exit.")
}
ids := make([]string, len(plan.ValidatorIds))
for i, id := range plan.ValidatorIds {
ids[i] = strconv.FormatUint(uint64(id), 10)
}
validatorIds := strings.Join(ids, ",")
can, err := rp.CanExitMegapoolDeficit(address, validatorIds)
if err != nil {
return err
}
if !can.CanExit {
return fmt.Errorf("the selected validators cannot be exited based on this megapool's deficit")
}
if can.ExitFee == nil {
return fmt.Errorf("the API did not return the total exit fee")
}

exitFeeEth := math.RoundDown(math.WeiToEth(can.ExitFee), 6)
fmt.Printf("Total EIP-7002 exit fee: %.6f ETH, in addition to transaction gas.\n", exitFeeEth)
if err := gas.AssignMaxFeeAndLimit(can.GasLimits, rp, yes); err != nil {
return err
}
if prompt.Declined(yes, "Force exit %d validators from megapool %s? This pays %.6f ETH in EIP-7002 exit fees plus gas.", len(can.ValidatorIds), address.Hex(), exitFeeEth) {
fmt.Println("Cancelled.")
return nil
}

result, err := rp.ExitMegapoolDeficit(address, validatorIds, can.ExitFee)
if err != nil {
return err
}
fmt.Println("Submitting the deficit-triggered exits...")
cliutils.PrintTransactionHash(rp, result.TxHash)
if _, err := rp.WaitForTransaction(result.TxHash); err != nil {
return err
}
fmt.Printf("Successfully submitted exit requests for %d validators. Beacon-chain exit processing is pending.\n", len(can.ValidatorIds))
return nil
}
Loading
Loading