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
15 changes: 15 additions & 0 deletions bindings/minipool/minipool-contract-v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type MinipoolV3 interface {
GetUserDistributed(opts *bind.CallOpts) (bool, error)
EstimateDistributeBalanceGas(rewardsOnly bool, opts *bind.TransactOpts) (rocketpool.GasInfo, error)
DistributeBalance(rewardsOnly bool, opts *bind.TransactOpts) (common.Hash, error)
PrepareDistributeBalance(rewardsOnly bool, opts *bind.TransactOpts) (*types.Transaction, error)
}

// Minipool contract
Expand Down Expand Up @@ -388,6 +389,20 @@ func (mp *minipool_v3) DistributeBalance(rewardsOnly bool, opts *bind.TransactOp
return tx.Hash(), nil
}

// PrepareDistributeBalance is like DistributeBalance but forces NoSend and returns the signed transaction
// (instead of sending it). Useful for assembling Flashbots bundles.
func (mp *minipool_v3) PrepareDistributeBalance(rewardsOnly bool, opts *bind.TransactOpts) (*types.Transaction, error) {
if opts == nil {
opts = &bind.TransactOpts{}
}
opts.NoSend = true
tx, err := mp.Contract.Transact(opts, "distributeBalance", rewardsOnly)
if err != nil {
return nil, fmt.Errorf("error preparing distribute tx for minipool %s: %w", mp.Address.Hex(), err)
}
return tx, nil
}

// Estimate the gas of Stake
func (mp *minipool_v3) EstimateStakeGas(validatorSignature rptypes.ValidatorSignature, depositDataRoot common.Hash, opts *bind.TransactOpts) (rocketpool.GasInfo, error) {
return mp.Contract.GetTransactionGasInfo(opts, "stake", validatorSignature[:], depositDataRoot)
Expand Down
15 changes: 15 additions & 0 deletions bindings/node/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"

"github.com/rocket-pool/smartnode/bindings/rocketpool"
)
Expand Down Expand Up @@ -62,6 +63,20 @@ func (d *Distributor) Distribute(opts *bind.TransactOpts) (common.Hash, error) {
return tx.Hash(), nil
}

// PrepareDistribute is like Distribute but forces NoSend and returns the signed transaction
// (instead of sending it). Useful for assembling Flashbots bundles.
func (d *Distributor) PrepareDistribute(opts *bind.TransactOpts) (*types.Transaction, error) {
if opts == nil {
opts = &bind.TransactOpts{}
}
opts.NoSend = true
tx, err := d.Contract.Transact(opts, "distribute")
if err != nil {
return nil, fmt.Errorf("error preparing distribute tx for distributor %s: %w", d.Address.Hex(), err)
}
return tx, nil
}

// Gets the node share of the distributor's current balance
func (d *Distributor) GetNodeShare(opts *bind.CallOpts) (*big.Int, error) {
nodeShare := new(*big.Int)
Expand Down
4 changes: 2 additions & 2 deletions rocketpool-cli/minipool/close.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/rocket-pool/smartnode/shared/utils/math"
)

func closeMinipools(minipool string, confirmSlashing, yes bool) error {
func closeMinipools(minipool string, confirmSlashing, yes, bundle bool) error {

// Get RP client
rp, err := rocketpool.NewClient().WithReady()
Expand Down Expand Up @@ -272,7 +272,7 @@ func closeMinipools(minipool string, confirmSlashing, yes bool) error {
// Close minipools
for _, minipool := range selectedMinipools {

response, err := rp.CloseMinipool(minipool.Address)
response, err := rp.CloseMinipool(minipool.Address, bundle)
if err != nil {
fmt.Printf("Could not close minipool %s: %s.\n", minipool.Address.Hex(), err.Error())
continue
Expand Down
7 changes: 6 additions & 1 deletion rocketpool-cli/minipool/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ func RegisterCommands(app *cli.Command, name string, aliases []string) {
Name: "confirm-slashing",
Usage: "Reserved for acknowledging situations where you've been slashed by the Beacon Chain, and closing a minipool will result in the complete loss of the ETH bond and your RPL collateral. DO NOT use this flag unless you have been explicitly instructed to do so.",
},
&cli.BoolFlag{
Name: "bundle",
Aliases: []string{"b"},
Usage: "Force closing via a Flashbots bundle (distribute + finalise). Without this flag, a bundle is still used automatically if your fee distributor has a balance. Mainnet only.",
},
},
Action: func(ctx context.Context, c *cli.Command) error {

Expand All @@ -281,7 +286,7 @@ func RegisterCommands(app *cli.Command, name string, aliases []string) {
}

// Run
return closeMinipools(c.String("minipool"), c.Bool("confirm-slashing"), c.Bool("yes"))
return closeMinipools(c.String("minipool"), c.Bool("confirm-slashing"), c.Bool("yes"), c.Bool("bundle"))

},
},
Expand Down
94 changes: 89 additions & 5 deletions rocketpool/api/minipool/close.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup"

Expand All @@ -18,9 +19,13 @@ import (

"github.com/rocket-pool/smartnode/shared/services"
"github.com/rocket-pool/smartnode/shared/services/beacon"
"github.com/rocket-pool/smartnode/shared/services/flashbots"
"github.com/rocket-pool/smartnode/shared/types/api"
)

// Gas can't be estimated against current chain state (the fee distributor's distribute() in the bundle hasn't executed yet)
const distributeBalanceBundleGasLimit uint64 = 600000

func getMinipoolCloseDetailsForNode(c *cli.Command) (*api.GetMinipoolCloseDetailsForNodeResponse, error) {

// Get services
Expand Down Expand Up @@ -307,7 +312,7 @@ func getMinipoolCloseDetails(rp *rocketpool.RocketPool, minipoolAddress common.A

}

func closeMinipool(c *cli.Command, minipoolAddress common.Address, opts *bind.TransactOpts) (*api.CloseMinipoolResponse, error) {
func closeMinipool(c *cli.Command, minipoolAddress common.Address, opts *bind.TransactOpts, bundle bool) (*api.CloseMinipoolResponse, error) {

// Get services
if err := services.RequireNodeRegistered(c); err != nil {
Expand Down Expand Up @@ -373,12 +378,91 @@ func closeMinipool(c *cli.Command, minipoolAddress common.Address, opts *bind.Tr
}
response.TxHash = hash
} else {
// Do a distribution, which will finalize it
hash, err := mpv3.DistributeBalance(false, opts)
cfg, err := services.GetConfig(c)
if err != nil {
return nil, err
return nil, fmt.Errorf("error getting config: %w", err)
}
response.TxHash = hash

ec, err := services.GetEthClient(c)
if err != nil {
return nil, fmt.Errorf("error getting eth client: %w", err)
}

relayUrl := cfg.Smartnode.GetFlashbotsRelayUrl()
useBundle := bundle
if useBundle && relayUrl == "" {
return nil, fmt.Errorf("a bundle was requested but Flashbots bundles are only supported on mainnet; there is no relay for this network")
}

var distributorAddress common.Address
if relayUrl != "" {
w, err := services.GetWallet(c)
if err != nil {
return nil, err
}
nodeAccount, err := w.GetNodeAccount()
if err != nil {
return nil, err
}
distributorAddress, err = node.GetDistributorAddress(rp, nodeAccount.Address, nil)
if err != nil {
return nil, fmt.Errorf("error getting fee distributor address: %w", err)
}

if !useBundle {
distributorBalance, err := ec.BalanceAt(context.Background(), distributorAddress, nil)
if err != nil {
return nil, fmt.Errorf("error getting fee distributor balance: %w", err)
}
useBundle = distributorBalance.Cmp(big.NewInt(1)) == 0
}
}

if !useBundle {
// Do a plain distribution, which will finalize it
hash, err := mpv3.DistributeBalance(false, opts)
if err != nil {
return nil, err
}
response.TxHash = hash
return &response, nil
}

// Empty the fee distributor and distribute the minipool balance (which also finalizes
// it) atomically in the same block via a Flashbots bundle.
distributor, err := node.NewDistributor(rp, distributorAddress, nil)
if err != nil {
return nil, fmt.Errorf("error creating fee distributor binding: %w", err)
}

// First tx: fee distributor distribute()
distributorTx, err := distributor.PrepareDistribute(opts)
if err != nil {
return nil, fmt.Errorf("error preparing fee distributor distribute tx for bundle: %w", err)
}

// Second tx: minipool distributeBalance(), with bumped nonce and a fixed gas limit
opts.Nonce = new(big.Int).SetUint64(distributorTx.Nonce() + 1)
opts.GasLimit = distributeBalanceBundleGasLimit

distBalTx, err := mpv3.PrepareDistributeBalance(false, opts)
if err != nil {
return nil, fmt.Errorf("error preparing distribute balance tx for bundle on minipool %s: %w", minipoolAddress.Hex(), err)
}

// Send the 2-tx bundle (distribute then distributeBalance)
timeoutCtx, cancel := context.WithTimeout(context.Background(), flashbots.DefaultSubmissionTimeout)
success, err := flashbots.SubmitBundleAndWait(timeoutCtx, nil, ec, relayUrl, []*gethtypes.Transaction{distributorTx, distBalTx}, flashbots.DefaultBundleBlockCount)
cancel()
if err != nil {
return nil, fmt.Errorf("error sending bundle for distribute+distributeBalance: %w", err)
}
if !success {
return nil, fmt.Errorf("bundle for minipool %s distribute+distributeBalance was not included. Bundles usually require a higher priority fee to get included", minipoolAddress.Hex())
}

// Report the distributeBalance tx hash (the last tx in the bundle).
response.TxHash = distBalTx.Hash()
}

// Return response
Expand Down
3 changes: 2 additions & 1 deletion rocketpool/api/minipool/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ func RegisterRoutes(mux *http.ServeMux, c *cli.Command) {
apiutils.WriteErrorResponse(w, err)
return
}
resp, err := closeMinipool(c, addr, opts)
bundle := r.FormValue("bundle") == "true"
resp, err := closeMinipool(c, addr, opts, bundle)
apiutils.WriteResponse(w, resp, err)
})

Expand Down
11 changes: 11 additions & 0 deletions shared/services/config/smartnode-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ type SmartnodeConfig struct {

// The FlashBots Protect RPC endpoint
flashbotsProtectUrl map[config.Network]string `yaml:"-"`

// The Flashbots relay URL for eth_sendBundle / bundle operations (distinct from Protect RPC)
flashbotsRelayUrl map[config.Network]string `yaml:"-"`
}

// Generates a newSmart Node configuration
Expand Down Expand Up @@ -635,6 +638,10 @@ func NewSmartnodeConfig(cfg *RocketPoolConfig) *SmartnodeConfig {
config.Network_Devnet: "https://rpc-hoodi.flashbots.net/",
config.Network_Testnet: "https://rpc-hoodi.flashbots.net/",
},

flashbotsRelayUrl: map[config.Network]string{
config.Network_Mainnet: "https://relay.flashbots.net",
},
}

}
Expand Down Expand Up @@ -993,6 +1000,10 @@ func (cfg *SmartnodeConfig) GetFlashbotsProtectUrl() string {
return cfg.flashbotsProtectUrl[cfg.Network.Value.(config.Network)]
}

func (cfg *SmartnodeConfig) GetFlashbotsRelayUrl() string {
return cfg.flashbotsRelayUrl[cfg.Network.Value.(config.Network)]
}

func getNetworkOptions() []config.ParameterOption {
options := []config.ParameterOption{
{
Expand Down
Loading
Loading