Skip to content

Commit

Permalink
Merge pull request #704 from rocket-pool/v2-deposit-saturn0
Browse files Browse the repository at this point in the history
V2 Saturn 0 changes + node `deposit` and `stake-rpl` updates
  • Loading branch information
0xfornax authored Nov 21, 2024
2 parents d2e7568 + 928d284 commit 0729581
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 109 deletions.
26 changes: 17 additions & 9 deletions rocketpool-cli/commands/node/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package node

import (
"fmt"
"strconv"
"strings"

"github.com/urfave/cli/v2"

Expand Down Expand Up @@ -240,7 +242,7 @@ func RegisterCommands(app *cli.App, name string, aliases []string) {
&cli.StringFlag{
Name: amountFlag,
Aliases: []string{"a"},
Usage: "The amount of RPL to stake (also accepts 'min8' / 'max8' for 8-ETH minipools, 'min16' / 'max16' for 16-ETH minipools, or 'all' for all of your RPL)",
Usage: "The amount of RPL to stake (also accepts custom percentages for 8-ETH minipools (eg. 3% of borrowed ETH as RPL), or 'all' for all of your RPL)",
},
cliutils.YesFlag,
&cli.BoolFlag{
Expand All @@ -254,14 +256,20 @@ func RegisterCommands(app *cli.App, name string, aliases []string) {
utils.ValidateArgCount(c, 0)

// Validate flags
if c.String(amountFlag) != "" &&
c.String(amountFlag) != "min8" &&
c.String(amountFlag) != "max8" &&
c.String(amountFlag) != "min16" &&
c.String(amountFlag) != "max16" &&
c.String(amountFlag) != "all" {
if _, err := input.ValidatePositiveEthAmount("stake amount", c.String(amountFlag)); err != nil {
return err
amount := c.String("amount")
if amount != "" {
if strings.HasSuffix(amount, "%") {
trimmedAmount := strings.TrimSuffix(amount, "%")
stakePercent, err := strconv.ParseFloat(trimmedAmount, 64)
if err != nil || stakePercent <= 0 {
return fmt.Errorf("invalid percentage value: %s", amount)
}

} else if amount != "all" {
// Validate it as a positive ETH amount if it's not a percentage or "all"
if _, err := input.ValidatePositiveEthAmount("stake amount", c.String(amountFlag)); err != nil {
return err
}
}
}

Expand Down
55 changes: 34 additions & 21 deletions rocketpool-cli/commands/node/deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
maxSlippageFlag string = "max-slippage"
saltFlag string = "salt"
defaultMaxNodeFeeSlippage float64 = 0.01 // 1% below current network fee
depositWarningMessage string = "NOTE: by creating a new minipool, your node will automatically initialize voting power to itself. If you would like to delegate your on-chain voting power, you should run the command `rocketpool pdao initialize-voting` before creating a new minipool."
depositWarningMessage string = "NOTE: By creating a new minipool, your node will automatically initialize voting power to itself. If you would like to delegate your on-chain voting power, you should run the command `rocketpool pdao initialize-voting` before creating a new minipool."
)

type deposit struct {
Expand All @@ -45,6 +45,32 @@ func newDepositPrompts(c *cli.Context, rp *client.Client, soloConversionPubkey *
return nil, nil
}

// Get the node's registration status
smoothie, err := rp.Api.Node.SetSmoothingPoolRegistrationState(true)
if err != nil {
return nil, err
}
if smoothie.Data.NodeRegistered {
fmt.Println("Your node is currently opted into the smoothing pool.")
} else if !smoothie.Data.NodeRegistered {
fmt.Println("Your node is not opted into the smoothing pool.")
}
fmt.Println()

// Post a warning about ETH only minipools
if !(c.Bool("yes") || utils.Confirm(fmt.Sprintf("%sNOTE: We’re excited to announce that newly launched Saturn 0 minipools will feature a commission structure ranging from 5%% to 14%%.\n\n- 5%% base commission\n- 5%% dynamic commission boost until Saturn 1\n- Up to 4%% boost for staked RPL valued at ≥10%% of borrowed ETH\n\n- Smoothing pool participation is required to benefit from dynamic commission\n- Dynamic commission starts when reward tree v10 is released (currently in development)\n- Dynamic commission ends soon after Saturn 1 is released\n\nNewly launched minipools with no RPL staked receive 10%% commission while newly launched minipools with ≥10%% of borrowed ETH staked receive 14%% commission.\n\nTo learn more about Saturn 0 and how it affects newly launched minipools, visit: https://rpips.rocketpool.net/tokenomics-explainers/005-rework-prelude%s\nWould you like to continue?", terminal.ColorYellow, terminal.ColorReset))) {
fmt.Println("Cancelled.")
return nil, err
}

// Post a final warning about the dynamic comission boost
if !smoothie.Data.NodeRegistered {
if !(c.Bool("yes") || utils.Confirm(fmt.Sprintf("%sWARNING: Your node is not opted into the smoothing pool, which means newly launched minipools will not benefit from the 5-9%% dynamic commission boost. You can join the smoothing pool using: 'rocketpool node join-smoothing-pool'.\n%sAre you sure you'd like to continue without opting into the smoothing pool?", terminal.ColorRed, terminal.ColorReset))) {
fmt.Println("Cancelled.")
return nil, err
}
}

// If hotfix is live and voting isn't initialized, display a warning
err = warnIfVotingUninitialized(rp, c, depositWarningMessage)
if err != nil {
Expand All @@ -69,7 +95,7 @@ func newDepositPrompts(c *cli.Context, rp *client.Client, soloConversionPubkey *

if soloConversionPubkey != nil {
// Print a notification about the pubkey
fmt.Printf("You are about to convert the solo staker %s into a Rocket Pool minipool. This will convert your 32 ETH deposit into either an 8 ETH or 16 ETH deposit (your choice), and convert the remaining 24 or 16 ETH into a deposit from the Rocket Pool staking pool. The staking pool portion will be credited to your node's account, allowing you to create more validators without depositing additional ETH onto the Beacon Chain. Your excess balance (your existing Beacon rewards) will be preserved and not shared with the pool stakers.\n", soloConversionPubkey.Hex())
fmt.Printf("You are about to convert the solo staker %s into a Rocket Pool minipool. This will convert your 32 ETH deposit into an 8 ETH deposit, and convert the remaining 24 ETH into a deposit from the Rocket Pool staking pool. The staking pool portion will be credited to your node's account, allowing you to create more validators without depositing additional ETH onto the Beacon Chain. Your excess balance (your existing Beacon rewards) will be preserved and not shared with the pool stakers.\n", soloConversionPubkey.Hex())
fmt.Println()
fmt.Println("Please thoroughly read our documentation at https://docs.rocketpool.net/guides/atlas/solo-staker-migration.html to learn about the process and its implications.")
fmt.Println()
Expand All @@ -91,25 +117,12 @@ func newDepositPrompts(c *cli.Context, rp *client.Client, soloConversionPubkey *
}
amount = depositAmount
} else {
// Get deposit amount options
amountOptions := []string{
"8 ETH",
"16 ETH",
}

// Prompt for amount
var selected int
if soloConversionPubkey != nil {
selected, _ = utils.Select("Please choose an amount of ETH you want to use as your deposit for the new minipool (this will become your share of the balance, and the remainder will become the pool stakers' share):", amountOptions)
} else {
selected, _ = utils.Select("Please choose an amount of ETH to deposit:", amountOptions)
}
switch selected {
case 0:
amount = 8
case 1:
amount = 16
// Post a warning about deposit size
if !(c.Bool("yes") || utils.Confirm(fmt.Sprintf("%sNOTE: You are about to make an 8 ETH deposit.%s\nWould you like to continue?", terminal.ColorYellow, terminal.ColorReset))) {
fmt.Println("Cancelled.")
return nil, nil
}
amount = 8
}

amountWei := eth.EthToWei(amount)
Expand Down Expand Up @@ -144,7 +157,7 @@ func newDepositPrompts(c *cli.Context, rp *client.Client, soloConversionPubkey *
} else {
// Prompt for min node fee
if nodeFeeResponse.Data.MinNodeFee.Cmp(nodeFeeResponse.Data.MaxNodeFee) == 0 {
fmt.Printf("Your minipool will use the current fixed commission rate of %.2f%%.\n", eth.WeiToEth(nodeFeeResponse.Data.MinNodeFee)*100)
fmt.Printf("Your minipool will use the current base commission rate of %.2f%%.\n", eth.WeiToEth(nodeFeeResponse.Data.MinNodeFee)*100)
minNodeFee = eth.WeiToEth(nodeFeeResponse.Data.MinNodeFee)
} else {
minNodeFee = promptMinNodeFee(eth.WeiToEth(nodeFeeResponse.Data.NodeFee), eth.WeiToEth(nodeFeeResponse.Data.MinNodeFee))
Expand Down
94 changes: 65 additions & 29 deletions rocketpool-cli/commands/node/stake-rpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"math/big"
"strconv"
"strings"

"github.com/ethereum/go-ethereum/common"
"github.com/rocket-pool/node-manager-core/eth"
Expand All @@ -12,13 +13,14 @@ import (
"github.com/rocket-pool/node-manager-core/utils/math"
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/client"
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils"
cliutils "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils"
"github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils/tx"
"github.com/rocket-pool/smartnode/v2/shared/types/api"
)

const (
swapFlag string = "swap"
stakeRPLWarningMessage string = "NOTE: by staking RPL, your node will automatically initialize voting power to itself. If you would like to delegate your on-chain voting power, you should run the command `rocketpool pdao initialize-voting` before staking RPL."
stakeRPLWarningMessage string = "NOTE: By staking RPL, your node will automatically initialize voting power to itself. If you would like to delegate your on-chain voting power, you should run the command `rocketpool pdao initialize-voting` before staking RPL."
)

func nodeStakeRpl(c *cli.Context) error {
Expand Down Expand Up @@ -64,28 +66,35 @@ func nodeStakeRpl(c *cli.Context) error {
if err != nil {
return fmt.Errorf("error getting RPL price: %w", err)
}

// Get stake amount
var amountWei *big.Int
switch c.String(amountFlag) {
case "min8":
amountWei = priceResponse.Data.MinPer8EthMinipoolRplStake
case "min16":
amountWei = priceResponse.Data.MinPer16EthMinipoolRplStake
case "all":
amountWei = rplBalance
case "":
amountWei, err = promptForRplAmount(priceResponse.Data, rplBalance)
var stakePercent float64

// Amount flag custom percentage input
if strings.HasSuffix(c.String("amount"), "%") {
_, err := fmt.Sscanf(c.String("amount"), "%f%%", &stakePercent)
if err != nil {
return err
return fmt.Errorf("invalid percentage format '%s': %w", c.String("amount"), err)
}
default:
amountWei = rplStakeForLEB8(eth.EthToWei(stakePercent/100), priceResponse.Data.RplPrice)

} else if c.String("amount") == "all" {
// Set amount to node's entire RPL balance
amountWei = rplBalance

} else if c.String("amount") != "" {
// Parse amount
stakeAmount, err := strconv.ParseFloat(c.String(amountFlag), 64)
stakeAmount, err := strconv.ParseFloat(c.String("amount"), 64)
if err != nil {
return fmt.Errorf("invalid stake amount '%s': %w", c.String(amountFlag), err)
return fmt.Errorf("Invalid stake amount '%s': %w", c.String("amount"), err)
}
amountWei = eth.EthToWei(stakeAmount)

} else {
amountWei, err = promptForRplAmount(priceResponse.Data, rplBalance, stakePercent)
if err != nil {
return err
}
}

// Build the stake TX
Expand Down Expand Up @@ -155,37 +164,64 @@ func nodeStakeRpl(c *cli.Context) error {
}

// Prompt the user for the amount of RPL to stake
func promptForRplAmount(priceResponse *api.NetworkRplPriceData, rplBalance *big.Int) (*big.Int, error) {
// Get min/max per minipool RPL stake amounts
minAmount8 := priceResponse.MinPer8EthMinipoolRplStake
minAmount16 := priceResponse.MinPer16EthMinipoolRplStake
func promptForRplAmount(priceResponse *api.NetworkRplPriceData, rplBalance *big.Int, stakePercent float64) (*big.Int, error) {
// Get the RPL stake amounts for 5,10,15% borrowed ETH per LEB8
fivePercentBorrowedPerMinipool := new(big.Int)
fivePercentBorrowedPerMinipool.SetString("50000000000000000", 10)
fivePercentBorrowedRplStake := rplStakeForLEB8(fivePercentBorrowedPerMinipool, priceResponse.RplPrice)
tenPercentBorrowedRplStake := new(big.Int).Mul(fivePercentBorrowedRplStake, big.NewInt(2))
fifteenPercentBorrowedRplStake := new(big.Int).Mul(fivePercentBorrowedRplStake, big.NewInt(3))

// Prompt for amount option
var amountWei *big.Int
amountOptions := []string{
fmt.Sprintf("The minimum minipool stake amount for an 8-ETH minipool (%.6f RPL)?", math.RoundUp(eth.WeiToEth(minAmount8), 6)),
fmt.Sprintf("The minimum minipool stake amount for a 16-ETH minipool (%.6f RPL)?", math.RoundUp(eth.WeiToEth(minAmount16), 6)),
fmt.Sprintf("5%% of borrowed ETH (%.6f RPL) for one minipool?", math.RoundUp(eth.WeiToEth(fivePercentBorrowedRplStake), 6)),
fmt.Sprintf("10%% of borrowed ETH (%.6f RPL) for one minipool?", math.RoundUp(eth.WeiToEth(tenPercentBorrowedRplStake), 6)),
fmt.Sprintf("15%% of borrowed ETH (%.6f RPL) for one minipool?", math.RoundUp(eth.WeiToEth(fifteenPercentBorrowedRplStake), 6)),
fmt.Sprintf("Your entire RPL balance (%.6f RPL)?", math.RoundDown(eth.WeiToEth(rplBalance), 6)),
"A custom amount",
}
selected, _ := utils.Select("Please choose an amount of RPL to stake:", amountOptions)
switch selected {
case 0:
amountWei = minAmount8
amountWei = fivePercentBorrowedRplStake
case 1:
amountWei = minAmount16
amountWei = tenPercentBorrowedRplStake
case 2:
amountWei = fifteenPercentBorrowedRplStake
case 3:
amountWei = rplBalance
}

// Prompt for custom amount
// Prompt for custom amount or percentage
if amountWei == nil {
inputAmount := utils.Prompt("Please enter an amount of RPL to stake:", "^\\d+(\\.\\d+)?$", "Invalid amount")
stakeAmount, err := strconv.ParseFloat(inputAmount, 64)
if err != nil {
return nil, fmt.Errorf("invalid stake amount '%s': %w", inputAmount, err)
inputAmountOrPercent := cliutils.Prompt("Please enter an amount of RPL or percentage of borrowed ETH to stake. (e.g '50' for 50 RPL or '5%' for 5% borrowed ETH as RPL):", "^(0|[1-9]\\d*)(\\.\\d+)?%?$", "Invalid amount")
if strings.HasSuffix(inputAmountOrPercent, "%") {
_, err := fmt.Sscanf(inputAmountOrPercent, "%f%%", &stakePercent)
if err != nil {
return nil, fmt.Errorf("Invalid percentage format '%s': %w", inputAmountOrPercent, err)
}
amountWei = rplStakeForLEB8(eth.EthToWei(stakePercent/100), priceResponse.RplPrice)
fmt.Println(amountWei)
} else {
stakeAmount, err := strconv.ParseFloat(inputAmountOrPercent, 64)
if err != nil {
return nil, fmt.Errorf("Invalid stake amount '%s': %w", inputAmountOrPercent, err)
}
amountWei = eth.EthToWei(stakeAmount)
}
amountWei = eth.EthToWei(stakeAmount)
}
return amountWei, nil

}

func rplStakeForLEB8(borrowedPerMinipool *big.Int, rplPrice *big.Int) *big.Int {
percentBorrowedRplStake := big.NewInt(0)
percentBorrowedRplStake.Mul(eth.EthToWei(24), borrowedPerMinipool)
percentBorrowedRplStake.Div(percentBorrowedRplStake, rplPrice)
percentBorrowedRplStake.Add(percentBorrowedRplStake, big.NewInt(1))
amountWei := percentBorrowedRplStake

return amountWei

}
28 changes: 0 additions & 28 deletions rocketpool-cli/commands/node/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,6 @@ func getStatus(c *cli.Context) error {
math.RoundDown(eth.WeiToEth(status.Data.EffectiveRplStake), 6))
if status.Data.BorrowedCollateralRatio > 0 {
rplTooLow := (status.Data.RplStake.Cmp(status.Data.MinimumRplStake) < 0)
rplTotalStake := math.RoundDown(eth.WeiToEth(status.Data.RplStake), 6)
rplWithdrawalLimit := math.RoundDown(eth.WeiToEth(status.Data.MaximumRplStake), 6)
if rplTooLow {
fmt.Printf(
"This is currently %s%.2f%% of its borrowed ETH%s and %.2f%% of its bonded ETH.\n",
Expand All @@ -302,35 +300,9 @@ func getStatus(c *cli.Context) error {
"This is currently %.2f%% of its borrowed ETH and %.2f%% of its bonded ETH.\n",
status.Data.BorrowedCollateralRatio*100, status.Data.BondedCollateralRatio*100)
}
fmt.Printf(
"It must keep at least %.6f RPL staked to claim RPL rewards (10%% of borrowed ETH).\n", math.RoundDown(eth.WeiToEth(status.Data.MinimumRplStake), 6))
fmt.Printf(
"RPIP-30 is in effect and the node will gradually earn rewards in amounts above the previous limit of %.6f RPL (150%% of bonded ETH). Read more at https://github.com/rocket-pool/RPIPs/blob/main/RPIPs/RPIP-30.md\n", math.RoundDown(eth.WeiToEth(status.Data.MaximumRplStake), 6))
if rplTotalStake > rplWithdrawalLimit {
fmt.Printf(
"You can now withdraw down to %.6f RPL (%.0f%% of bonded eth)\n",
math.RoundDown(eth.WeiToEth(status.Data.MaximumRplStake), 6),
eth.WeiToEth(status.Data.MaximumStakeFraction)*100)
}
if rplTooLow {
fmt.Printf("%sWARNING: you are currently undercollateralized. You must stake at least %.6f more RPL in order to claim RPL rewards.%s\n", terminal.ColorRed, math.RoundUp(eth.WeiToEth(big.NewInt(0).Sub(status.Data.MinimumRplStake, status.Data.RplStake)), 6), terminal.ColorReset)
}
}
fmt.Println()

remainingAmount := big.NewInt(0).Sub(status.Data.EthMatchedLimit, status.Data.EthMatched)
remainingAmount.Sub(remainingAmount, status.Data.PendingMatchAmount)
remainingAmountEth := int(eth.WeiToEth(remainingAmount))
remainingFor8EB := remainingAmountEth / 24
if remainingFor8EB < 0 {
remainingFor8EB = 0
}
remainingFor16EB := remainingAmountEth / 16
if remainingFor16EB < 0 {
remainingFor16EB = 0
}
fmt.Printf("The node has enough RPL staked to make %d more 8-ETH minipools (or %d more 16-ETH minipools).\n\n", remainingFor8EB, remainingFor16EB)

// Minipool details
fmt.Printf("%s=== Minipools ===%s\n", terminal.ColorGreen, terminal.ColorReset)
if status.Data.MinipoolCounts.Total > 0 {
Expand Down
18 changes: 0 additions & 18 deletions rocketpool-daemon/api/network/rpl-price.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,27 +84,9 @@ func (c *networkPriceContext) GetState(mc *batch.MultiCaller) {

func (c *networkPriceContext) PrepareData(data *api.NetworkRplPriceData, opts *bind.TransactOpts) (types.ResponseStatus, error) {
var rplPrice *big.Int
_24Eth := eth.EthToWei(24)
_16Eth := eth.EthToWei(16)
var minPerMinipoolStake *big.Int

data.RplPriceBlock = c.networkMgr.PricesBlock.Formatted()
rplPrice = c.networkMgr.RplPrice.Raw()
minPerMinipoolStake = c.pSettings.Node.MinimumPerMinipoolStake.Raw()

// Min for LEB8s
minPer8EthMinipoolRplStake := big.NewInt(0)
minPer8EthMinipoolRplStake.Mul(_24Eth, minPerMinipoolStake) // Min is 10% of borrowed (24 ETH)
minPer8EthMinipoolRplStake.Div(minPer8EthMinipoolRplStake, rplPrice)
minPer8EthMinipoolRplStake.Add(minPer8EthMinipoolRplStake, big.NewInt(1))
data.MinPer8EthMinipoolRplStake = minPer8EthMinipoolRplStake

// Min for 16s
minPer16EthMinipoolRplStake := big.NewInt(0)
minPer16EthMinipoolRplStake.Mul(_16Eth, minPerMinipoolStake) // Min is 10% of borrowed (16 ETH)
minPer16EthMinipoolRplStake.Div(minPer16EthMinipoolRplStake, rplPrice)
minPer16EthMinipoolRplStake.Add(minPer16EthMinipoolRplStake, big.NewInt(1))
data.MinPer16EthMinipoolRplStake = minPer16EthMinipoolRplStake

// Update & return response
data.RplPrice = rplPrice
Expand Down
6 changes: 2 additions & 4 deletions shared/types/api/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ type NetworkNodeFeeData struct {
}

type NetworkRplPriceData struct {
RplPrice *big.Int `json:"rplPrice"`
RplPriceBlock uint64 `json:"rplPriceBlock"`
MinPer8EthMinipoolRplStake *big.Int `json:"minPer8EthMinipoolRplStake"`
MinPer16EthMinipoolRplStake *big.Int `json:"minPer16EthMinipoolRplStake"`
RplPrice *big.Int `json:"rplPrice"`
RplPriceBlock uint64 `json:"rplPriceBlock"`
}

type NetworkStatsData struct {
Expand Down

0 comments on commit 0729581

Please sign in to comment.