Skip to content

Commit 66b4254

Browse files
Add configurable rebalance strategy for bridge pegout
Introduce UTXO_SPLIT strategy that splits bridge conversions into multiple transactions based on BridgeTransactionMin, alongside the existing ALL_AT_ONCE behavior. Strategy is configured via REBALANCE_STRATEGY env variable.
1 parent 826afc3 commit 66b4254

File tree

11 files changed

+562
-15
lines changed

11 files changed

+562
-15
lines changed

cmd/application/lps/application.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ func NewApplication(initCtx context.Context, env environment.Environment, timeou
8383
liquidityProvider := registry.NewLiquidityProvider(dbRegistry, rootstockRegistry, btcRegistry, messagingRegistry)
8484
mutexes := environment.NewApplicationMutexes()
8585

86-
useCaseRegistry := registry.NewUseCaseRegistry(env, rootstockRegistry, btcRegistry, dbRegistry, liquidityProvider, messagingRegistry, mutexes)
86+
useCaseRegistry, err := registry.NewUseCaseRegistry(env, rootstockRegistry, btcRegistry, dbRegistry, liquidityProvider, messagingRegistry, mutexes)
87+
if err != nil {
88+
log.Fatal("Error creating use case registry:", err)
89+
}
8790
watcherRegistry := registry.NewWatcherRegistry(env, useCaseRegistry, rootstockRegistry, btcRegistry, liquidityProvider, messagingRegistry, watcher.NewApplicationTickers(), timeouts)
8891
return &Application{
8992
env: env,

docker-compose/local/docker-compose.lps.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ services:
8787
- BTC_RELEASE_WATCHER_START_BLOCK
8888
- BTC_RELEASE_WATCHER_PAGE_SIZE
8989
- BTC_RELEASE_CHECK_TIMEOUT
90+
- REBALANCE_STRATEGY
9091
ports:
9192
- "8080:8080"
9293
volumes:

internal/adapters/entrypoints/watcher/pegout_bridge_watcher_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestPegoutBridgeWatcher_Start(t *testing.T) {
3535
bridge := &mocks.BridgeMock{}
3636
bridge.On("GetAddress").Return(test.AnyAddress)
3737
mutexes := environment.NewApplicationMutexes()
38-
bridgeUseCase := pegout.NewBridgePegoutUseCase(pegoutRepository, providerMock, rskWallet, blockchain.RskContracts{Bridge: bridge}, mutexes.RskWalletMutex())
38+
bridgeUseCase := pegout.NewBridgePegoutUseCase(pegoutRepository, providerMock, rskWallet, blockchain.RskContracts{Bridge: bridge}, mutexes.RskWalletMutex(), pegout.AllAtOnce)
3939
getUseCase := w.NewGetWatchedPegoutQuoteUseCase(pegoutRepository)
4040
bridgeWatcher := watcher.NewPegoutBridgeWatcher(getUseCase, bridgeUseCase, ticker)
4141
resetMocks := func() {

internal/configuration/environment/environment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ type PegoutEnv struct {
168168
DepositCacheStartBlock uint64 `env:"PEGOUT_DEPOSIT_CACHE_START_BLOCK"`
169169
BtcReleaseWatcherStartBlock uint64 `env:"BTC_RELEASE_WATCHER_START_BLOCK"`
170170
BtcReleaseWatcherPageSize uint64 `env:"BTC_RELEASE_WATCHER_PAGE_SIZE"`
171+
RebalanceStrategy string `env:"REBALANCE_STRATEGY" validate:"required,oneof=ALL_AT_ONCE UTXO_SPLIT"`
171172
}
172173

173174
type CaptchaEnv struct {

internal/configuration/environment/environment_reader_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func setUpEnv(t *testing.T) {
3434
"BTC_RELEASE_WATCHER_START_BLOCK": "1",
3535
"USE_SEGWIT_FEDERATION": "true",
3636
"ALLOWED_ORIGINS": "http://example.com,http://example2.com",
37+
"REBALANCE_STRATEGY": "ALL_AT_ONCE",
3738
}
3839
const envFilePath = "../../../sample-config.env"
3940
envFile, err := os.ReadFile(envFilePath)

internal/configuration/registry/usecase.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ func NewUseCaseRegistry(
9292
liquidityProvider *dataproviders.LocalLiquidityProvider,
9393
messaging *Messaging,
9494
mutexes entities.ApplicationMutexes,
95-
) *UseCaseRegistry {
95+
) (*UseCaseRegistry, error) {
96+
rebalanceStrategy, err := pegout.ParseRebalanceStrategy(env.Pegout.RebalanceStrategy)
97+
if err != nil {
98+
return nil, err
99+
}
96100
return &UseCaseRegistry{
97101
summariesUseCase: reports.NewSummariesUseCase(
98102
databaseRegistry.PeginRepository,
@@ -259,6 +263,7 @@ func NewUseCaseRegistry(
259263
rskRegistry.Wallet,
260264
rskRegistry.Contracts,
261265
mutexes.RskWalletMutex(),
266+
rebalanceStrategy,
262267
),
263268
peginStatusUseCase: pegin.NewStatusUseCase(databaseRegistry.PeginRepository),
264269
pegoutStatusUseCase: pegout.NewStatusUseCase(databaseRegistry.PegoutRepository),
@@ -348,7 +353,7 @@ func NewUseCaseRegistry(
348353
messaging.Rpc,
349354
utils.Scale,
350355
),
351-
}
356+
}, nil
352357
}
353358

354359
func (registry *UseCaseRegistry) GetPeginQuoteUseCase() *pegin.GetQuoteUseCase {

internal/configuration/registry/usecase_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func TestNewUseCaseRegistry(t *testing.T) {
3030
BridgeAddress: "0x0000000000000000000000000000000001000006",
3131
},
3232
Btc: environment.BtcEnv{Network: "testnet"},
33+
Pegout: environment.PegoutEnv{RebalanceStrategy: "ALL_AT_ONCE"},
3334
}
3435

3536
client := &mocks.DbClientBindingMock{}
@@ -55,8 +56,8 @@ func TestNewUseCaseRegistry(t *testing.T) {
5556
lp := registry.NewLiquidityProvider(dbRegistry, rskRegistry, btcRegistry, messagingRegistry)
5657
mutexes := environment.NewApplicationMutexes()
5758

58-
useCaseRegistry := registry.NewUseCaseRegistry(env, rskRegistry, btcRegistry, dbRegistry, lp, messagingRegistry, mutexes)
59-
59+
useCaseRegistry, err := registry.NewUseCaseRegistry(env, rskRegistry, btcRegistry, dbRegistry, lp, messagingRegistry, mutexes)
60+
require.NoError(t, err)
6061
require.NotNil(t, useCaseRegistry)
6162
value := reflect.ValueOf(useCaseRegistry).Elem()
6263
for i := 0; i < value.NumField(); i++ {

internal/configuration/registry/watcher_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestNewWatcherRegistry(t *testing.T) {
2929
BridgeAddress: "0x0000000000000000000000000000000001000006",
3030
},
3131
Btc: environment.BtcEnv{Network: "testnet"},
32+
Pegout: environment.PegoutEnv{RebalanceStrategy: "ALL_AT_ONCE"},
3233
}
3334

3435
client := &mocks.DbClientBindingMock{}
@@ -53,7 +54,8 @@ func TestNewWatcherRegistry(t *testing.T) {
5354
messagingRegistry := registry.NewMessagingRegistry(context.Background(), environment.Environment{}, rskClient, connection, registry.ExternalRpc{})
5455
lp := registry.NewLiquidityProvider(dbRegistry, rskRegistry, btcRegistry, messagingRegistry)
5556
mutexes := environment.NewApplicationMutexes()
56-
useCaseRegistry := registry.NewUseCaseRegistry(env, rskRegistry, btcRegistry, dbRegistry, lp, messagingRegistry, mutexes)
57+
useCaseRegistry, err := registry.NewUseCaseRegistry(env, rskRegistry, btcRegistry, dbRegistry, lp, messagingRegistry, mutexes)
58+
require.NoError(t, err)
5759

5860
watcherRegistry := registry.NewWatcherRegistry(env, useCaseRegistry, rskRegistry, btcRegistry, lp, messagingRegistry, watcher.NewApplicationTickers(), environment.DefaultTimeouts())
5961

internal/usecases/pegout/bridge_pegout.go

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pegout
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"sync"
78

89
"github.com/rsksmart/liquidity-provider-server/internal/entities"
@@ -20,12 +21,31 @@ const (
2021
BridgeConversionGasPrice = 60000000
2122
)
2223

24+
type RebalanceStrategy string
25+
26+
const (
27+
AllAtOnce RebalanceStrategy = "ALL_AT_ONCE"
28+
UtxoSplit RebalanceStrategy = "UTXO_SPLIT"
29+
)
30+
31+
func ParseRebalanceStrategy(s string) (RebalanceStrategy, error) {
32+
switch s {
33+
case string(AllAtOnce):
34+
return AllAtOnce, nil
35+
case string(UtxoSplit):
36+
return UtxoSplit, nil
37+
default:
38+
return "", fmt.Errorf("unknown rebalance strategy: %q", s)
39+
}
40+
}
41+
2342
type BridgePegoutUseCase struct {
2443
quoteRepository quote.PegoutQuoteRepository
2544
pegoutProvider liquidity_provider.PegoutLiquidityProvider
2645
rskWallet blockchain.RootstockWallet
2746
contracts blockchain.RskContracts
2847
rskWalletMutex sync.Locker
48+
strategy RebalanceStrategy
2949
}
3050

3151
func NewBridgePegoutUseCase(
@@ -34,19 +54,21 @@ func NewBridgePegoutUseCase(
3454
rskWallet blockchain.RootstockWallet,
3555
contracts blockchain.RskContracts,
3656
rskWalletMutex sync.Locker,
57+
strategy RebalanceStrategy,
3758
) *BridgePegoutUseCase {
3859
return &BridgePegoutUseCase{
3960
quoteRepository: quoteRepository,
4061
pegoutProvider: pegoutProvider,
4162
rskWallet: rskWallet,
4263
contracts: contracts,
4364
rskWalletMutex: rskWalletMutex,
65+
strategy: strategy,
4466
}
4567
}
4668

4769
func (useCase *BridgePegoutUseCase) Run(ctx context.Context, watchedQuotes ...quote.WatchedPegoutQuote) error {
4870
var err error
49-
var balance, totalValue *entities.Wei
71+
var totalValue *entities.Wei
5072

5173
totalValue, err = useCase.calculateTotalToPegout(watchedQuotes)
5274
if err != nil {
@@ -66,6 +88,18 @@ func (useCase *BridgePegoutUseCase) Run(ctx context.Context, watchedQuotes ...qu
6688
useCase.rskWalletMutex.Lock()
6789
defer useCase.rskWalletMutex.Unlock()
6890

91+
switch useCase.strategy {
92+
case UtxoSplit:
93+
return useCase.runUtxoSplit(ctx, totalValue, pegoutConfig, watchedQuotes)
94+
default:
95+
return useCase.runAllAtOnce(ctx, totalValue, watchedQuotes)
96+
}
97+
}
98+
99+
func (useCase *BridgePegoutUseCase) runAllAtOnce(ctx context.Context, totalValue *entities.Wei, watchedQuotes []quote.WatchedPegoutQuote) error {
100+
var balance *entities.Wei
101+
var err error
102+
69103
requiredBalance := new(entities.Wei).Add(totalValue, entities.NewWei(BridgeConversionGasLimit*BridgeConversionGasPrice))
70104
if balance, err = useCase.rskWallet.GetBalance(ctx); err != nil {
71105
return usecases.WrapUseCaseError(usecases.BridgePegoutId, err)
@@ -86,6 +120,77 @@ func (useCase *BridgePegoutUseCase) Run(ctx context.Context, watchedQuotes ...qu
86120
return nil
87121
}
88122

123+
func (useCase *BridgePegoutUseCase) runUtxoSplit(
124+
ctx context.Context,
125+
totalValue *entities.Wei,
126+
pegoutConfig liquidity_provider.PegoutConfiguration,
127+
watchedQuotes []quote.WatchedPegoutQuote,
128+
) error {
129+
var balance *entities.Wei
130+
var err error
131+
132+
bridgeMin := pegoutConfig.BridgeTransactionMin
133+
numTxs, _ := new(entities.Wei).Div(totalValue, bridgeMin)
134+
remainder := new(entities.Wei).Sub(totalValue, new(entities.Wei).Mul(numTxs, bridgeMin))
135+
136+
gasPerTx := entities.NewWei(BridgeConversionGasLimit * BridgeConversionGasPrice)
137+
requiredBalance := new(entities.Wei).Add(totalValue, new(entities.Wei).Mul(numTxs, gasPerTx))
138+
if balance, err = useCase.rskWallet.GetBalance(ctx); err != nil {
139+
return usecases.WrapUseCaseError(usecases.BridgePegoutId, err)
140+
} else if balance.Cmp(requiredBalance) < 0 {
141+
return usecases.WrapUseCaseError(usecases.BridgePegoutId, usecases.InsufficientAmountError)
142+
}
143+
144+
bridgeAddress := useCase.contracts.Bridge.GetAddress()
145+
n := numTxs.Uint64()
146+
147+
if n == 1 {
148+
config := blockchain.NewTransactionConfig(totalValue, BridgeConversionGasLimit, entities.NewWei(BridgeConversionGasPrice))
149+
receipt, txErr := useCase.rskWallet.SendRbtc(ctx, config, bridgeAddress)
150+
if txErr == nil {
151+
log.Debugf("%s: transaction sent to the bridge successfully (%s)", usecases.BridgePegoutId, receipt.TransactionHash)
152+
}
153+
err = useCase.updateQuotes(ctx, receipt, txErr, watchedQuotes)
154+
if err != nil {
155+
return usecases.WrapUseCaseError(usecases.BridgePegoutId, err)
156+
}
157+
return nil
158+
}
159+
160+
// N >= 2: first chunk absorbs the remainder
161+
firstChunk := new(entities.Wei).Add(bridgeMin.Copy(), remainder)
162+
var receipt blockchain.TransactionReceipt
163+
var txErr error
164+
165+
config := blockchain.NewTransactionConfig(firstChunk, BridgeConversionGasLimit, entities.NewWei(BridgeConversionGasPrice))
166+
receipt, txErr = useCase.rskWallet.SendRbtc(ctx, config, bridgeAddress)
167+
if txErr == nil {
168+
log.Debugf("%s: split tx 1/%d sent to the bridge successfully (%s)", usecases.BridgePegoutId, n, receipt.TransactionHash)
169+
} else {
170+
err = useCase.updateQuotes(ctx, receipt, txErr, watchedQuotes)
171+
if err != nil {
172+
return usecases.WrapUseCaseError(usecases.BridgePegoutId, err)
173+
}
174+
return usecases.WrapUseCaseError(usecases.BridgePegoutId, txErr)
175+
}
176+
177+
for i := uint64(1); i < n; i++ {
178+
config = blockchain.NewTransactionConfig(bridgeMin.Copy(), BridgeConversionGasLimit, entities.NewWei(BridgeConversionGasPrice))
179+
receipt, txErr = useCase.rskWallet.SendRbtc(ctx, config, bridgeAddress)
180+
if txErr == nil {
181+
log.Debugf("%s: split tx %d/%d sent to the bridge successfully (%s)", usecases.BridgePegoutId, i+1, n, receipt.TransactionHash)
182+
} else {
183+
break
184+
}
185+
}
186+
187+
err = useCase.updateQuotes(ctx, receipt, txErr, watchedQuotes)
188+
if err != nil {
189+
return usecases.WrapUseCaseError(usecases.BridgePegoutId, err)
190+
}
191+
return nil
192+
}
193+
89194
func (useCase *BridgePegoutUseCase) updateQuotes(
90195
ctx context.Context,
91196
receipt blockchain.TransactionReceipt,

0 commit comments

Comments
 (0)