Skip to content

Commit bfeabc7

Browse files
committed
auto_balance_test and handle auto-unbond and auto-bridge concurrency
1 parent 6b36b1a commit bfeabc7

6 files changed

Lines changed: 136 additions & 31 deletions

File tree

cmd/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ func init() {
138138
rootCmd.Flags().Duration("refresh-gas-estimates-interval", 12*time.Hour, "Interval for resetting cached gas estimates and gas-adjustment levels (<=0 disables)")
139139

140140
// Auto-bridge: keep wallet at a fixed balance by bridging the excess to Ethereum
141-
rootCmd.Flags().Uint64("auto-balance-to-keep", 0, "Keep this amount of loya in the wallet; bridge any excess to Ethereum at --auto-balance-execution-time (0 = disabled)")
142-
rootCmd.Flags().String("auto-balance-execution-time", "00:00", "UTC time to execute the auto-balance bridge (HH:MM, e.g. '03:00')")
143-
rootCmd.Flags().String("auto-balance-bridge-to-eth-addr", "", "Ethereum address to bridge excess tokens to (required when auto-balance-to-keep > 0)")
141+
rootCmd.Flags().Uint64(daemonflags.FlagAutoBalanceToKeep, 0, "Keep this amount of loya in the wallet; bridge any excess to Ethereum at --auto-balance-execution-time (0 = disabled)")
142+
rootCmd.Flags().String(daemonflags.FlagAutoBalanceExecutionTime, "00:00", "UTC time to execute the auto-balance bridge (HH:MM, e.g. '03:00')")
143+
rootCmd.Flags().String(daemonflags.FlagAutoBalanceBridgeToEthAddr, "", "Ethereum address to bridge excess tokens to (required when auto-balance-to-keep > 0)")
144144

145145
// Marking required flags
146146
if err := rootCmd.MarkFlagRequired(flags.FlagHome); err != nil {

flags/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const (
1717
FlagPriceDaemonLoopDelayMs = "price-daemon-loop-delay-ms"
1818

1919
FlagKeyringBackend = "keyring-backend"
20+
21+
FlagAutoBalanceToKeep = "auto-balance-to-keep"
22+
FlagAutoBalanceExecutionTime = "auto-balance-execution-time"
23+
FlagAutoBalanceBridgeToEthAddr = "auto-balance-bridge-to-eth-addr"
2024
)
2125

2226
// Shared flags contains configuration flags shared by all daemons.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package client
2+
3+
import (
4+
"testing"
5+
6+
"cosmossdk.io/math"
7+
8+
"github.com/stretchr/testify/require"
9+
bridgetypes "github.com/tellor-io/layer/x/bridge/types"
10+
oracletypes "github.com/tellor-io/layer/x/oracle/types"
11+
)
12+
13+
func TestNormalizeAutoBalanceEthAddr(t *testing.T) {
14+
addr, err := normalizeAutoBalanceEthAddr("0x0000000000000000000000000000000000000001")
15+
require.NoError(t, err)
16+
require.Equal(t, "0000000000000000000000000000000000000001", addr)
17+
18+
addr, err = normalizeAutoBalanceEthAddr("0000000000000000000000000000000000000001")
19+
require.NoError(t, err)
20+
require.Equal(t, "0000000000000000000000000000000000000001", addr)
21+
22+
addr, err = normalizeAutoBalanceEthAddr("")
23+
require.NoError(t, err)
24+
require.Empty(t, addr)
25+
26+
_, err = normalizeAutoBalanceEthAddr("not-an-address")
27+
require.Error(t, err)
28+
}
29+
30+
func TestParseAutoBalanceExecutionTime(t *testing.T) {
31+
hour, minute, err := parseAutoBalanceExecutionTime("03:05")
32+
require.NoError(t, err)
33+
require.Equal(t, 3, hour)
34+
require.Equal(t, 5, minute)
35+
36+
_, _, err = parseAutoBalanceExecutionTime("24:00")
37+
require.Error(t, err)
38+
39+
_, _, err = parseAutoBalanceExecutionTime("03")
40+
require.Error(t, err)
41+
}
42+
43+
func TestIsBridgeDepositReportMsg(t *testing.T) {
44+
require.True(t, isBridgeDepositReportMsg(&oracletypes.MsgSubmitValue{}))
45+
require.False(t, isBridgeDepositReportMsg(&bridgetypes.MsgWithdrawTokens{}))
46+
}
47+
48+
func TestShouldSkipAutoUnbond(t *testing.T) {
49+
reporterStake := math.LegacyNewDec(1_000)
50+
unbondAmount := math.NewInt(100)
51+
tenPercent, err := math.LegacyNewDecFromStr("0.10")
52+
require.NoError(t, err)
53+
fivePercent, err := math.LegacyNewDecFromStr("0.05")
54+
require.NoError(t, err)
55+
56+
require.False(t, shouldSkipAutoUnbond(reporterStake, math.LegacyZeroDec(), unbondAmount), "zero max percentage disables the cap")
57+
require.False(t, shouldSkipAutoUnbond(reporterStake, tenPercent, unbondAmount))
58+
require.True(t, shouldSkipAutoUnbond(reporterStake, fivePercent, unbondAmount))
59+
}

reporter/client/broadcast_message.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,13 @@ func (c *Client) BroadcastTxMsgToChain(ctx context.Context) {
222222
txCtx, cancel := context.WithTimeout(ctx, txBroadcastTimeout)
223223
defer cancel()
224224

225-
if !txInfo.isBridge {
226-
_, err := c.sendTx(txCtx, txInfo.QueryMetaId, false, txInfo.Msg)
225+
if txInfo.isBridge && isBridgeDepositReportMsg(txInfo.Msg) {
226+
c.HandleBridgeDepositTxInChannel(txCtx, txInfo)
227+
} else {
228+
_, err := c.sendTx(txCtx, txInfo.QueryMetaId, txInfo.isBridge, txInfo.Msg)
227229
if err != nil {
228230
c.logger.Error(fmt.Sprintf("Error sending tx: %v", err))
229231
}
230-
} else {
231-
c.HandleBridgeDepositTxInChannel(txCtx, txInfo)
232232
}
233233
}(obj)
234234

@@ -237,3 +237,8 @@ func (c *Client) BroadcastTxMsgToChain(ctx context.Context) {
237237
}
238238
}
239239
}
240+
241+
func isBridgeDepositReportMsg(msg interface{}) bool {
242+
_, ok := msg.(*oracletypes.MsgSubmitValue)
243+
return ok
244+
}

reporter/client/client.go

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"strconv"
78
"strings"
89
"sync"
910
"sync/atomic"
1011
"time"
1112

1213
rpchttp "github.com/cometbft/cometbft/rpc/client/http"
14+
"github.com/ethereum/go-ethereum/common"
1315
"github.com/spf13/viper"
1416
globalfeetypes "github.com/strangelove-ventures/globalfee/x/globalfee/types"
1517
customquery "github.com/tellor-io/layer-daemons/custom_query"
16-
"github.com/tellor-io/layer-daemons/flags"
18+
daemonflags "github.com/tellor-io/layer-daemons/flags"
1719
pricefeedtypes "github.com/tellor-io/layer-daemons/pricefeed/client/types"
1820
pricefeedservertypes "github.com/tellor-io/layer-daemons/server/types/pricefeed"
1921
tokenbridgetypes "github.com/tellor-io/layer-daemons/server/types/token_bridge"
@@ -122,7 +124,7 @@ func NewClient(logger log.Logger, valGasMin string) *Client {
122124

123125
func (c *Client) Start(
124126
ctx context.Context,
125-
flags flags.DaemonFlags,
127+
flags daemonflags.DaemonFlags,
126128
grpcAddress string,
127129
grpcClient daemontypes.GrpcClient,
128130
marketParams []pricefeedtypes.MarketParam,
@@ -211,6 +213,9 @@ func (c *Client) Start(
211213
c.refreshGasEstimatesInterval = viper.GetDuration("refresh-gas-estimates-interval")
212214

213215
if autoUnbondingFrequency > 0 {
216+
if autoUnbondingFrequency > 21 {
217+
return fmt.Errorf("auto-unbonding-frequency must be between 1 and 21 days when set, got: %d", autoUnbondingFrequency)
218+
}
214219
if autoUnbondingAmount == 0 {
215220
return fmt.Errorf("auto-unbonding-amount must be greater than 0 when auto-unbonding-frequency is set")
216221
}
@@ -245,15 +250,21 @@ func (c *Client) Start(
245250
}
246251

247252
// Read and validate auto-balance-to-keep configuration
248-
autoBalanceToKeep := viper.GetUint64("auto-balance-to-keep")
249-
autoBalanceEthAddr := strings.TrimPrefix(viper.GetString("auto-balance-eth-addr"), "0x")
253+
autoBalanceToKeep := viper.GetUint64(daemonflags.FlagAutoBalanceToKeep)
250254
if autoBalanceToKeep > 0 {
255+
autoBalanceEthAddr, err := normalizeAutoBalanceEthAddr(viper.GetString(daemonflags.FlagAutoBalanceBridgeToEthAddr))
256+
if err != nil {
257+
return err
258+
}
251259
if autoBalanceEthAddr == "" {
252-
return fmt.Errorf("auto-balance-eth-addr is required when auto-balance-to-keep > 0")
260+
return fmt.Errorf("%s is required when %s > 0", daemonflags.FlagAutoBalanceBridgeToEthAddr, daemonflags.FlagAutoBalanceToKeep)
261+
}
262+
if _, _, err := parseAutoBalanceExecutionTime(viper.GetString(daemonflags.FlagAutoBalanceExecutionTime)); err != nil {
263+
return err
253264
}
254265
c.logger.Info("Auto balance-to-keep enabled",
255266
"balance_to_keep_loya", autoBalanceToKeep,
256-
"execution_time", viper.GetString("auto-balance-execution-time"),
267+
"execution_time", viper.GetString(daemonflags.FlagAutoBalanceExecutionTime),
257268
"eth_addr", "0x"+autoBalanceEthAddr,
258269
)
259270
} else {
@@ -312,7 +323,7 @@ func (c *Client) Start(
312323
func StartReporterDaemonTaskLoop(
313324
client *Client,
314325
ctx context.Context,
315-
flags flags.DaemonFlags,
326+
flags daemonflags.DaemonFlags,
316327
wg *sync.WaitGroup,
317328
) {
318329
reporterCreated := false
@@ -477,6 +488,30 @@ func (c *Client) trySend(ctx context.Context, info TxChannelInfo) bool {
477488
}
478489
}
479490

491+
func normalizeAutoBalanceEthAddr(addr string) (string, error) {
492+
addr = strings.TrimSpace(addr)
493+
if addr == "" {
494+
return "", nil
495+
}
496+
if !common.IsHexAddress(addr) {
497+
return "", fmt.Errorf("%s must be a valid Ethereum address, got: %s", daemonflags.FlagAutoBalanceBridgeToEthAddr, addr)
498+
}
499+
return strings.TrimPrefix(common.HexToAddress(addr).Hex(), "0x"), nil
500+
}
501+
502+
func parseAutoBalanceExecutionTime(executionTime string) (int, int, error) {
503+
parts := strings.SplitN(executionTime, ":", 2)
504+
if len(parts) != 2 {
505+
return 0, 0, fmt.Errorf("invalid %s, expected HH:MM, got: %s", daemonflags.FlagAutoBalanceExecutionTime, executionTime)
506+
}
507+
hour, errH := strconv.Atoi(parts[0])
508+
minute, errM := strconv.Atoi(parts[1])
509+
if errH != nil || errM != nil || hour < 0 || hour > 23 || minute < 0 || minute > 59 {
510+
return 0, 0, fmt.Errorf("invalid %s value, expected HH:MM in UTC, got: %s", daemonflags.FlagAutoBalanceExecutionTime, executionTime)
511+
}
512+
return hour, minute, nil
513+
}
514+
480515
// Stop stops the reporter client gracefully
481516
func (c *Client) Stop() {
482517
c.stopOnce.Do(func() {

reporter/client/reporter_monitors.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ethereum/go-ethereum/accounts/abi"
1515
"github.com/shirou/gopsutil/v3/process"
1616
"github.com/spf13/viper"
17+
"github.com/tellor-io/layer-daemons/flags"
1718
tokenbridgetipstypes "github.com/tellor-io/layer-daemons/server/types/token_bridge_tips"
1819
oracletypes "github.com/tellor-io/layer/x/oracle/types"
1920
reportertypes "github.com/tellor-io/layer/x/reporter/types"
@@ -305,9 +306,8 @@ func (c *Client) AutoUnbondStakePeriodically(ctx context.Context, wg *sync.WaitG
305306
}
306307
}
307308

308-
maxStakeAbleToWithdraw := reporterStake.Mul(maxStakePercentage)
309-
310-
if maxStakeAbleToWithdraw.LT(math.LegacyNewDecFromInt(unbondAmount)) {
309+
if shouldSkipAutoUnbond(reporterStake, maxStakePercentage, unbondAmount) {
310+
maxStakeAbleToWithdraw := reporterStake.Mul(maxStakePercentage)
311311
c.logger.Info("Not enough stake to withdraw", "reporterStake", reporterStake, "maxStakeAbleToWithdraw", maxStakeAbleToWithdraw)
312312
continue
313313
}
@@ -323,31 +323,33 @@ func (c *Client) AutoUnbondStakePeriodically(ctx context.Context, wg *sync.WaitG
323323
}
324324
}
325325

326+
func shouldSkipAutoUnbond(reporterStake math.LegacyDec, maxStakePercentage math.LegacyDec, unbondAmount math.Int) bool {
327+
if !maxStakePercentage.GT(math.LegacyZeroDec()) {
328+
return false
329+
}
330+
return reporterStake.Mul(maxStakePercentage).LT(math.LegacyNewDecFromInt(unbondAmount))
331+
}
332+
326333
// AutoBridgeWalletExcessPeriodically watches the wallet balance once per day at the configured
327334
// UTC time. Whenever the balance exceeds --auto-balance-to-keep (loya), the excess (minus a
328-
// 1 TRB gas reserve) is bridged to the Ethereum address supplied by --auto-balance-eth-addr.
335+
// 1 TRB gas reserve) is bridged to the Ethereum address supplied by --auto-balance-bridge-to-eth-addr.
329336
func (c *Client) AutoBridgeWalletExcessPeriodically(ctx context.Context, wg *sync.WaitGroup) {
330337
defer wg.Done()
331338

332-
balanceToKeep := viper.GetUint64("auto-balance-to-keep")
339+
balanceToKeep := viper.GetUint64(flags.FlagAutoBalanceToKeep)
333340
if balanceToKeep == 0 {
334341
c.logger.Info("Auto balance-to-keep is disabled")
335342
return
336343
}
337344

338-
ethAddr := strings.TrimPrefix(viper.GetString("auto-balance-eth-addr"), "0x")
339-
executionTime := viper.GetString("auto-balance-execution-time")
340-
341-
// Parse HH:MM
342-
parts := strings.SplitN(executionTime, ":", 2)
343-
if len(parts) != 2 {
344-
c.logger.Error("invalid auto-balance-execution-time, expected HH:MM", "value", executionTime)
345+
ethAddr, err := normalizeAutoBalanceEthAddr(viper.GetString(flags.FlagAutoBalanceBridgeToEthAddr))
346+
if err != nil {
347+
c.logger.Error("invalid auto-balance bridge address", "error", err)
345348
return
346349
}
347-
hour, errH := strconv.Atoi(parts[0])
348-
minute, errM := strconv.Atoi(parts[1])
349-
if errH != nil || errM != nil || hour < 0 || hour > 23 || minute < 0 || minute > 59 {
350-
c.logger.Error("invalid auto-balance-execution-time value", "value", executionTime)
350+
hour, minute, err := parseAutoBalanceExecutionTime(viper.GetString(flags.FlagAutoBalanceExecutionTime))
351+
if err != nil {
352+
c.logger.Error("invalid auto-balance execution time", "error", err)
351353
return
352354
}
353355

@@ -402,7 +404,7 @@ func (c *Client) AutoBridgeWalletExcessPeriodically(ctx context.Context, wg *syn
402404
Recipient: ethAddr,
403405
Amount: sdk.NewCoin("loya", amountToBridge),
404406
}
405-
c.txChan <- TxChannelInfo{Msg: msg, isBridge: true, NumRetries: 0, QueryMetaId: 0}
407+
c.trySend(ctx, TxChannelInfo{Msg: msg, isBridge: true, NumRetries: 0, QueryMetaId: 0})
406408
}
407409
}
408410

0 commit comments

Comments
 (0)