-
Notifications
You must be signed in to change notification settings - Fork 105
/
reduce-bond.go
404 lines (347 loc) · 14.6 KB
/
reduce-bond.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
package minipool
import (
"bytes"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
rocketpoolapi "github.com/rocket-pool/rocketpool-go/rocketpool"
"github.com/rocket-pool/rocketpool-go/types"
"github.com/rocket-pool/rocketpool-go/utils/eth"
"github.com/rocket-pool/smartnode/shared/services/gas"
"github.com/rocket-pool/smartnode/shared/services/rocketpool"
"github.com/rocket-pool/smartnode/shared/types/api"
cliutils "github.com/rocket-pool/smartnode/shared/utils/cli"
"github.com/rocket-pool/smartnode/shared/utils/math"
"github.com/urfave/cli"
)
func beginReduceBondAmount(c *cli.Context) error {
// Get RP client
rp, err := rocketpool.NewClientFromCtx(c).WithReady()
if err != nil {
return err
}
defer rp.Close()
// Check the fee distributor
distribResponse, err := rp.IsFeeDistributorInitialized()
if err != nil {
return fmt.Errorf("error checking the node's fee distributor status: %w", err)
}
if !distribResponse.IsInitialized {
fmt.Println("Minipools cannot have their bonds reduced until your fee distributor has been initialized.\nPlease run `rocketpool node initialize-fee-distributor` first, then return here to reduce your bonds.")
return nil
}
// Get minipool statuses
status, err := rp.MinipoolStatus()
if err != nil {
return err
}
// Get the bond reduction variables
settingsResponse, err := rp.GetTNDAOMinipoolSettings()
if err != nil {
return err
}
// TODO POST-ATLAS: Ask the user how much they want the new bond to be; since there's only one option right now there's no point
fmt.Printf("This will allow you to begin the bond reduction process to reduce your 16 ETH bond for a minipool down to 8 ETH, awarding you 8 ETH in credit and allowing you to create a second minipool for free (plus gas costs).\n\nThere will be a %.0f-hour wait period after you start the process. After this wait period is over, you will have %.0f hours to complete the process. Your `node` container will do this automatically unless you have it disabled, in which case you must manually run `rocketpool minipool reduce-bond`.\n\n%sNOTE: If you don't run it during this window, your request will time out and you will have to start over.%s\n\n", (time.Duration(settingsResponse.BondReductionWindowStart) * time.Second).Hours(), (time.Duration(settingsResponse.BondReductionWindowLength) * time.Second).Hours(), colorYellow, colorReset)
newBondAmount := eth.EthToWei(8)
// Prompt for confirmation
if !(c.Bool("yes") || cliutils.Confirm("Do you understand how the bond reduction process will work?")) {
fmt.Println("Cancelled.")
return nil
}
bondReductionTimeout := time.Duration(settingsResponse.BondReductionWindowStart+settingsResponse.BondReductionWindowLength) * time.Second
// Get reduceable minipools
reduceableMinipools := []api.MinipoolDetails{}
scrubbedMinipools := []api.MinipoolDetails{}
for _, minipool := range status.Minipools {
if minipool.ReduceBondCancelled {
scrubbedMinipools = append(scrubbedMinipools, minipool)
} else {
nodeDepositBalance := eth.WeiToEth(minipool.Node.DepositBalance)
if nodeDepositBalance == 16 &&
time.Since(minipool.ReduceBondTime) > bondReductionTimeout &&
minipool.Status.Status == types.Staking &&
!minipool.Finalised {
reduceableMinipools = append(reduceableMinipools, minipool)
}
}
}
// Print scrubs
if len(scrubbedMinipools) > 0 {
fmt.Printf("%sNOTE: The following minipools had a previous bond reducton attempt scrubbed by the Oracle DAO and are no longer reduceable:\n", colorYellow)
for _, mp := range scrubbedMinipools {
fmt.Printf("\t%s\n", mp.Address)
}
fmt.Printf("%s\n\n", colorReset)
}
if len(reduceableMinipools) == 0 {
fmt.Println("No minipools can have their bond reduced at this time.")
return nil
}
// Get selected minipools
var selectedMinipools []api.MinipoolDetails
if c.String("minipool") == "" {
// Prompt for minipool selection
options := make([]string, len(reduceableMinipools)+1)
options[0] = "All available minipools"
for mi, minipool := range reduceableMinipools {
options[mi+1] = fmt.Sprintf("%s (Current bond: %d ETH, commission: %.2f%%)", minipool.Address.Hex(), int(eth.WeiToEth(minipool.Node.DepositBalance)), minipool.Node.Fee*100)
}
selected, _ := cliutils.Select("Please select a minipool to begin the ETH bond reduction for:", options)
// Get minipools
if selected == 0 {
selectedMinipools = reduceableMinipools
} else {
selectedMinipools = []api.MinipoolDetails{reduceableMinipools[selected-1]}
}
} else {
// Get matching minipools
if c.String("minipool") == "all" {
selectedMinipools = reduceableMinipools
} else {
selectedAddress := common.HexToAddress(c.String("minipool"))
for _, minipool := range reduceableMinipools {
if bytes.Equal(minipool.Address.Bytes(), selectedAddress.Bytes()) {
selectedMinipools = []api.MinipoolDetails{minipool}
break
}
}
if selectedMinipools == nil {
return fmt.Errorf("The minipool %s cannot have its bond reduced.", selectedAddress.Hex())
}
}
}
// Get the total gas limit estimate
var totalGas uint64 = 0
var totalSafeGas uint64 = 0
var gasInfo rocketpoolapi.GasInfo
totalMatchRequest := big.NewInt(0)
for _, minipool := range selectedMinipools {
canResponse, err := rp.CanBeginReduceBondAmount(minipool.Address, newBondAmount)
if err != nil {
return fmt.Errorf("couldn't check if minipool %s could have its bond reduced: %s)", minipool.Address.Hex(), err.Error())
} else {
if !canResponse.CanReduce {
fmt.Printf("Cannot reduce bond for minipool %s:\n", minipool.Address.Hex())
if canResponse.BondReductionDisabled {
fmt.Println("Bond reductions are currently disabled.")
}
if canResponse.MinipoolVersionTooLow {
fmt.Println("The minipool version is too low. It must be upgraded first using `rocketpool minipool delegate-upgrade`.")
}
if canResponse.BalanceTooLow {
fmt.Printf("The minipool's validator balance on the Beacon Chain is too low (must be 32 ETH or higher, currently %.6f ETH).\n", math.RoundDown(float64(canResponse.Balance)/1e9, 6))
}
if canResponse.InvalidBeaconState {
fmt.Printf("The minipool's validator is not in a legal state on the Beacon Chain. It must be pending or active (current state: %s)\n", canResponse.BeaconState)
}
return nil
}
gasInfo = canResponse.GasInfo
totalGas += canResponse.GasInfo.EstGasLimit
totalSafeGas += canResponse.GasInfo.SafeGasLimit
totalMatchRequest.Add(totalMatchRequest, canResponse.MatchRequest)
}
}
gasInfo.EstGasLimit = totalGas
gasInfo.SafeGasLimit = totalSafeGas
// Make sure there's enough collateral to cover all of the pending bond reductions
collateralResponse, err := rp.CheckCollateral()
if err != nil {
return fmt.Errorf("error checking the node's total collateral: %w", err)
}
totalMatchAvailable := big.NewInt(0).Sub(collateralResponse.EthMatchedLimit, collateralResponse.EthMatched)
totalMatchAvailable.Sub(totalMatchAvailable, collateralResponse.PendingMatchAmount)
if totalMatchAvailable.Cmp(totalMatchRequest) < 0 {
fmt.Printf("You do not have enough RPL staked to support all of the selected bond reductions.\nYou can borrow %.6f more ETH, but are requesting %.6f ETH with these bond reductions.\nIn total, they would bring you below the minimum RPL staking requirement (including the RPL required for any pending bond reductions you've already started).\nYou will have to stake more RPL first.\n", eth.WeiToEth(totalMatchAvailable), eth.WeiToEth(totalMatchRequest))
return nil
}
// Assign max fees
err = gas.AssignMaxFeeAndLimit(gasInfo, rp, c.Bool("yes"))
if err != nil {
return err
}
// Prompt for confirmation
if !(c.Bool("yes") || cliutils.Confirm(fmt.Sprintf("Are you sure you want to begin bond reduction for %d minipools from 16 ETH to 8 ETH?", len(selectedMinipools)))) {
fmt.Println("Cancelled.")
return nil
}
// Begin bond reduction
for _, minipool := range selectedMinipools {
response, err := rp.BeginReduceBondAmount(minipool.Address, newBondAmount)
if err != nil {
fmt.Printf("Could not begin bond reduction for minipool %s: %s.\n", minipool.Address.Hex(), err.Error())
continue
}
fmt.Printf("Beginning bond reduction for minipool %s...\n", minipool.Address.Hex())
cliutils.PrintTransactionHash(rp, response.TxHash)
if _, err = rp.WaitForTransaction(response.TxHash); err != nil {
fmt.Printf("Could not begin bond reduction for minipool %s: %s.\n", minipool.Address.Hex(), err.Error())
} else {
fmt.Printf("Successfully started bond reduction for minipool %s.\n", minipool.Address.Hex())
}
}
// Return
return nil
}
func reduceBondAmount(c *cli.Context) error {
// Get RP client
rp, err := rocketpool.NewClientFromCtx(c).WithReady()
if err != nil {
return err
}
defer rp.Close()
// Get minipool statuses
status, err := rp.MinipoolStatus()
if err != nil {
return err
}
// Get the bond reduction variables
settingsResponse, err := rp.GetTNDAOMinipoolSettings()
if err != nil {
return err
}
fmt.Println("NOTE: this function is used to complete the bond reduction process for a minipool. If you haven't started the process already, please run `rocketpool minipool begin-bond-reduction` first.")
fmt.Println()
// Get reduceable minipools
reduceableMinipools := []api.MinipoolDetails{}
for _, minipool := range status.Minipools {
timeSinceBondReductionStart := time.Since(minipool.ReduceBondTime)
nodeDepositBalance := eth.WeiToEth(minipool.Node.DepositBalance)
if nodeDepositBalance == 16 && timeSinceBondReductionStart > (time.Duration(settingsResponse.BondReductionWindowStart)*time.Second) && timeSinceBondReductionStart < (time.Duration(settingsResponse.BondReductionWindowStart+settingsResponse.BondReductionWindowLength)*time.Second) && !minipool.ReduceBondCancelled {
reduceableMinipools = append(reduceableMinipools, minipool)
}
}
if len(reduceableMinipools) == 0 {
fmt.Println("No minipools can have their bond reduced at this time.")
return nil
}
// Workaround for the fee distribution issue
err = forceFeeDistribution(c, rp)
if err != nil {
return err
}
// Get selected minipools
var selectedMinipools []api.MinipoolDetails
if c.String("minipool") == "" {
// Prompt for minipool selection
options := make([]string, len(reduceableMinipools)+1)
options[0] = "All available minipools"
for mi, minipool := range reduceableMinipools {
options[mi+1] = fmt.Sprintf("%s (Current bond: %d ETH)", minipool.Address.Hex(), int(eth.WeiToEth(minipool.Node.DepositBalance)))
}
selected, _ := cliutils.Select("Please select a minipool to reduce the ETH bond for:", options)
// Get minipools
if selected == 0 {
selectedMinipools = reduceableMinipools
} else {
selectedMinipools = []api.MinipoolDetails{reduceableMinipools[selected-1]}
}
} else {
// Get matching minipools
if c.String("minipool") == "all" {
selectedMinipools = reduceableMinipools
} else {
selectedAddress := common.HexToAddress(c.String("minipool"))
for _, minipool := range reduceableMinipools {
if bytes.Equal(minipool.Address.Bytes(), selectedAddress.Bytes()) {
selectedMinipools = []api.MinipoolDetails{minipool}
break
}
}
if selectedMinipools == nil {
return fmt.Errorf("The minipool %s cannot have its bond reduced.", selectedAddress.Hex())
}
}
}
// Get the total gas limit estimate
var totalGas uint64 = 0
var totalSafeGas uint64 = 0
var gasInfo rocketpoolapi.GasInfo
for _, minipool := range selectedMinipools {
canResponse, err := rp.CanReduceBondAmount(minipool.Address)
if err != nil {
return fmt.Errorf("error checking if minipool %s can have its bond reduced: %w", minipool.Address.Hex(), err)
} else if !canResponse.CanReduce {
fmt.Printf("Minipool %s cannot have its bond reduced:\n", minipool.Address.Hex())
fmt.Println("The minipool version is too low. Please run `rocketpool minipool delegate-upgrade` to update it.")
return nil
} else {
gasInfo = canResponse.GasInfo
totalGas += canResponse.GasInfo.EstGasLimit
totalSafeGas += canResponse.GasInfo.SafeGasLimit
}
}
gasInfo.EstGasLimit = totalGas
gasInfo.SafeGasLimit = totalSafeGas
// Assign max fees
err = gas.AssignMaxFeeAndLimit(gasInfo, rp, c.Bool("yes"))
if err != nil {
return err
}
// Prompt for confirmation
if !(c.Bool("yes") || cliutils.Confirm(fmt.Sprintf("Are you sure you want to reduce the bond for %d minipools from 16 ETH to 8 ETH?", len(selectedMinipools)))) {
fmt.Println("Cancelled.")
return nil
}
// Begin bond reduction
for _, minipool := range selectedMinipools {
response, err := rp.ReduceBondAmount(minipool.Address)
if err != nil {
fmt.Printf("Could not reduce bond for minipool %s: %s.\n", minipool.Address.Hex(), err.Error())
continue
}
fmt.Printf("Reducing bond for minipool %s...\n", minipool.Address.Hex())
cliutils.PrintTransactionHash(rp, response.TxHash)
if _, err = rp.WaitForTransaction(response.TxHash); err != nil {
fmt.Printf("Could not reduce bond for minipool %s: %s.\n", minipool.Address.Hex(), err.Error())
} else {
fmt.Printf("Successfully reduced bond for minipool %s.\n", minipool.Address.Hex())
}
}
// Return
return nil
}
func forceFeeDistribution(c *cli.Context, rp *rocketpool.Client) error {
// Get the gas estimate
canDistributeResponse, err := rp.CanDistribute()
if err != nil {
return err
}
balance := eth.WeiToEth(canDistributeResponse.Balance)
if balance == 0 {
fmt.Println("Your fee distributor does not have any ETH and does not need to be distributed.")
fmt.Println()
return nil
}
fmt.Println("NOTE: prior to bond reduction, you must distribute the funds in your fee distributor.")
fmt.Println()
// Print info
rEthShare := balance - canDistributeResponse.NodeShare
fmt.Printf("Your fee distributor's balance of %.6f ETH will be distributed as follows:\n", balance)
fmt.Printf("\tYour withdrawal address will receive %.6f ETH.\n", canDistributeResponse.NodeShare)
fmt.Printf("\trETH pool stakers will receive %.6f ETH.\n\n", rEthShare)
// Assign max fees
err = gas.AssignMaxFeeAndLimit(canDistributeResponse.GasInfo, rp, c.Bool("yes"))
if err != nil {
return err
}
// Prompt for confirmation
if !(c.Bool("yes") || cliutils.Confirm("Are you sure you want to distribute the ETH from your node's fee distributor?")) {
fmt.Println("Cancelled.")
return nil
}
// Distribute
response, err := rp.Distribute()
if err != nil {
return err
}
fmt.Printf("Distributing rewards...\n")
cliutils.PrintTransactionHash(rp, response.TxHash)
if _, err = rp.WaitForTransaction(response.TxHash); err != nil {
return err
}
// Log & return
fmt.Println("Successfully distributed your fee distributor's balance. Your rewards should arrive in your withdrawal address shortly.")
return nil
}