Skip to content

Commit 8e87e96

Browse files
Merge pull request #926 from rsksmart/feat_fly2217_prioritize_pegin_contract_funds
Feat FLY2217 prioritize pegin contract funds in hot-to-cold transfers
2 parents c7bbe8b + 5f0f4c4 commit 8e87e96

File tree

10 files changed

+686
-11
lines changed

10 files changed

+686
-11
lines changed

internal/adapters/dataproviders/rootstock/bindings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type PeginBinding interface {
6868
CallForUser(opts *bind.TransactOpts, quote bindings.QuotesPegInQuote) (*types.Transaction, error)
6969
GetBalance(opts *bind.CallOpts, addr common.Address) (*big.Int, error)
7070
GetFeePercentage(opts *bind.CallOpts) (*big.Int, error)
71+
Withdraw(opts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error)
7172
}
7273

7374
type PeginContractAdapter interface {

internal/adapters/dataproviders/rootstock/pegin_contract.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,42 @@ func (peginContract *peginContractImpl) RegisterPegin(params blockchain.Register
240240
return transactionReceipt, nil
241241
}
242242

243+
// Withdraw withdraws the specified amount from the LP's balance in the pegin contract.
244+
// It first performs a dry-run call to check for reverts before submitting the actual transaction.
245+
func (peginContract *peginContractImpl) Withdraw(amount *entities.Wei) error {
246+
const functionName = "withdraw"
247+
var res []any
248+
249+
revert := peginContract.contract.Caller().Call(
250+
&bind.CallOpts{From: peginContract.signer.Address()}, &res, functionName, amount.AsBigInt(),
251+
)
252+
parsedRevert, err := ParseRevertReason(peginContract.abis.Flyover, revert)
253+
if err != nil && parsedRevert == nil {
254+
return fmt.Errorf("error parsing withdraw result: %w", err)
255+
} else if parsedRevert != nil {
256+
return fmt.Errorf("withdraw reverted with: %s", parsedRevert.Name)
257+
}
258+
259+
opts := &bind.TransactOpts{
260+
From: peginContract.signer.Address(),
261+
Signer: peginContract.signer.Sign,
262+
}
263+
264+
receipt, err := rskRetry(peginContract.retryParams.Retries, peginContract.retryParams.Sleep,
265+
func() (*geth.Receipt, error) {
266+
return awaitTx(peginContract.client, peginContract.miningTimeout, "Withdraw", func() (*geth.Transaction, error) {
267+
return peginContract.contract.Withdraw(opts, amount.AsBigInt())
268+
})
269+
})
270+
271+
if err != nil {
272+
return fmt.Errorf("withdraw error: %w", err)
273+
} else if receipt == nil || receipt.Status == 0 {
274+
return errors.New("withdraw error: transaction failed")
275+
}
276+
return nil
277+
}
278+
243279
// parsePeginQuote parses a quote.PeginQuote into a bindings.QuotesPegInQuote. All BTC address fields support all address types
244280
// except for FedBtcAddress which must be a P2SH address.
245281
func parsePeginQuote(peginQuote quote.PeginQuote) (bindings.QuotesPegInQuote, error) {

internal/adapters/dataproviders/rootstock/pegin_contract_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,93 @@ func TestPeginContractImpl_RegisterPegin_ErrorHandling(t *testing.T) {
425425
assert.Empty(t, result)
426426
})
427427
}
428+
429+
func TestPeginContractImpl_Withdraw(t *testing.T) {
430+
contractBinding := &mocks.PeginContractAdapterMock{}
431+
signerMock := &mocks.TransactionSignerMock{}
432+
mockClient := &mocks.RpcClientBindingMock{}
433+
callerMock := &mocks.ContractCallerBindingMock{}
434+
peginContract := rootstock.NewPeginContractImpl(
435+
rootstock.NewRskClient(mockClient),
436+
test.AnyAddress,
437+
contractBinding,
438+
signerMock,
439+
rootstock.RetryParams{},
440+
time.Duration(1),
441+
Abis,
442+
)
443+
withdrawAmount := entities.NewWei(5000000000000000000)
444+
matchOptsFunc := func(opts *bind.TransactOpts) bool {
445+
return opts.From.String() == parsedAddress.String()
446+
}
447+
t.Run("Success", func(t *testing.T) {
448+
contractBinding.On("Caller").Return(callerMock).Once()
449+
callerMock.On("Call", mock.Anything, mock.Anything, "withdraw", withdrawAmount.AsBigInt()).Return(nil).Once()
450+
tx := prepareTxMocks(mockClient, signerMock, true)
451+
contractBinding.On("Withdraw", mock.MatchedBy(matchOptsFunc), withdrawAmount.AsBigInt()).Return(tx, nil).Once()
452+
err := peginContract.Withdraw(withdrawAmount)
453+
require.NoError(t, err)
454+
contractBinding.AssertExpectations(t)
455+
callerMock.AssertExpectations(t)
456+
})
457+
}
458+
459+
// nolint:funlen
460+
func TestPeginContractImpl_Withdraw_ErrorHandling(t *testing.T) {
461+
contractBinding := &mocks.PeginContractAdapterMock{}
462+
signerMock := &mocks.TransactionSignerMock{}
463+
mockClient := &mocks.RpcClientBindingMock{}
464+
callerMock := &mocks.ContractCallerBindingMock{}
465+
peginContract := rootstock.NewPeginContractImpl(
466+
rootstock.NewRskClient(mockClient),
467+
test.AnyAddress,
468+
contractBinding,
469+
signerMock,
470+
rootstock.RetryParams{},
471+
time.Duration(1),
472+
Abis,
473+
)
474+
withdrawAmount := entities.NewWei(5000000000000000000)
475+
signerMock.On("Address").Return(parsedAddress)
476+
matchOptsFunc := func(opts *bind.TransactOpts) bool {
477+
return opts.From.String() == parsedAddress.String()
478+
}
479+
t.Run("Error handling (dry-run revert NoBalance)", func(t *testing.T) {
480+
e := NewRskRpcError("transaction reverted", "0x29226653")
481+
contractBinding.On("Caller").Return(callerMock).Once()
482+
callerMock.On("Call", mock.Anything, mock.Anything, "withdraw", withdrawAmount.AsBigInt()).Return(e).Once()
483+
err := peginContract.Withdraw(withdrawAmount)
484+
require.ErrorContains(t, err, "withdraw reverted with: NoBalance")
485+
contractBinding.AssertNotCalled(t, "Withdraw", mock.Anything, mock.Anything)
486+
contractBinding.AssertExpectations(t)
487+
callerMock.AssertExpectations(t)
488+
})
489+
t.Run("Error handling (dry-run parse error)", func(t *testing.T) {
490+
contractBinding.On("Caller").Return(callerMock).Once()
491+
callerMock.On("Call", mock.Anything, mock.Anything, "withdraw", withdrawAmount.AsBigInt()).Return(assert.AnError).Once()
492+
err := peginContract.Withdraw(withdrawAmount)
493+
require.ErrorContains(t, err, "error parsing withdraw result")
494+
contractBinding.AssertExpectations(t)
495+
callerMock.AssertExpectations(t)
496+
})
497+
t.Run("Error handling (transaction send error)", func(t *testing.T) {
498+
contractBinding.On("Caller").Return(callerMock).Once()
499+
callerMock.On("Call", mock.Anything, mock.Anything, "withdraw", withdrawAmount.AsBigInt()).Return(nil).Once()
500+
_ = prepareTxMocks(mockClient, signerMock, true)
501+
contractBinding.On("Withdraw", mock.MatchedBy(matchOptsFunc), withdrawAmount.AsBigInt()).Return(nil, assert.AnError).Once()
502+
err := peginContract.Withdraw(withdrawAmount)
503+
require.ErrorContains(t, err, "withdraw error")
504+
contractBinding.AssertExpectations(t)
505+
callerMock.AssertExpectations(t)
506+
})
507+
t.Run("Error handling (transaction reverted)", func(t *testing.T) {
508+
contractBinding.On("Caller").Return(callerMock).Once()
509+
callerMock.On("Call", mock.Anything, mock.Anything, "withdraw", withdrawAmount.AsBigInt()).Return(nil).Once()
510+
tx := prepareTxMocks(mockClient, signerMock, false)
511+
contractBinding.On("Withdraw", mock.MatchedBy(matchOptsFunc), withdrawAmount.AsBigInt()).Return(tx, nil).Once()
512+
err := peginContract.Withdraw(withdrawAmount)
513+
require.ErrorContains(t, err, "withdraw error: transaction failed")
514+
contractBinding.AssertExpectations(t)
515+
callerMock.AssertExpectations(t)
516+
})
517+
}

internal/configuration/registry/usecase.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ func NewUseCaseRegistry(
383383
lpRegistry.ColdWallet,
384384
btcRegistry.PaymentWallet,
385385
rskRegistry.Wallet,
386+
rskRegistry.Contracts,
386387
messaging.Rpc,
387388
mutexes.BtcWalletMutex(),
388389
mutexes.RskWalletMutex(),

internal/entities/blockchain/lbc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type PeginContract interface {
8585
CallForUser(txConfig TransactionConfig, peginQuote quote.PeginQuote) (TransactionReceipt, error)
8686
RegisterPegin(params RegisterPeginParams) (TransactionReceipt, error)
8787
DaoFeePercentage() (uint64, error)
88+
Withdraw(amount *entities.Wei) error
8889
}
8990

9091
type PegoutContract interface {

internal/usecases/liquidity_provider/transfer_excess_to_cold_wallet.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ type TransferToColdWalletResult struct {
5151
RskResult NetworkTransferResult
5252
}
5353

54+
type currentLiquidityResult struct {
55+
Btc *entities.Wei
56+
Rbtc *entities.Wei
57+
}
58+
5459
func NewTransferToColdWalletResult(btcResult, rskResult NetworkTransferResult) *TransferToColdWalletResult {
5560
return &TransferToColdWalletResult{
5661
BtcResult: btcResult,
@@ -79,6 +84,7 @@ type TransferExcessToColdWalletUseCase struct {
7984
coldWallet cold_wallet.ColdWallet
8085
btcWallet blockchain.BitcoinWallet
8186
rskWallet blockchain.RootstockWallet
87+
contracts blockchain.RskContracts
8288
rpc blockchain.Rpc
8389
btcWalletMutex sync.Locker
8490
rskWalletMutex sync.Locker
@@ -101,6 +107,7 @@ func NewTransferExcessToColdWalletUseCase(
101107
coldWallet cold_wallet.ColdWallet,
102108
btcWallet blockchain.BitcoinWallet,
103109
rskWallet blockchain.RootstockWallet,
110+
contracts blockchain.RskContracts,
104111
rpc blockchain.Rpc,
105112
btcWalletMutex sync.Locker,
106113
rskWalletMutex sync.Locker,
@@ -121,6 +128,7 @@ func NewTransferExcessToColdWalletUseCase(
121128
coldWallet: coldWallet,
122129
btcWallet: btcWallet,
123130
rskWallet: rskWallet,
131+
contracts: contracts,
124132
rpc: rpc,
125133
btcWalletMutex: btcWalletMutex,
126134
rskWalletMutex: rskWalletMutex,
@@ -349,6 +357,7 @@ func (useCase *TransferExcessToColdWalletUseCase) publishRskTransferEvent(ctx co
349357
}
350358
}
351359

360+
// sendTransferAlert triggers the alertSender to send an alert with the transfer details.
352361
func (useCase *TransferExcessToColdWalletUseCase) sendTransferAlert(ctx context.Context, asset string, amount *entities.Wei, txHash string, fee *entities.Wei, isTimeForcing bool) error {
353362
reason := "threshold"
354363
if isTimeForcing {
@@ -376,6 +385,9 @@ func (useCase *TransferExcessToColdWalletUseCase) handleRskTransfer(ctx context.
376385
}
377386
}
378387

388+
// In order to give priority to the contract funds, we withdraw them before the transfer
389+
useCase.withdrawContractFunds()
390+
379391
txResult, err := useCase.executeRskTransfer(ctx, excess)
380392
if err == nil {
381393
useCase.publishRskTransferEvent(ctx, txResult, isTimeForcingTransfer)
@@ -403,9 +415,22 @@ func (useCase *TransferExcessToColdWalletUseCase) handleRskTransfer(ctx context.
403415
}
404416
}
405417

406-
type currentLiquidityResult struct {
407-
Btc *entities.Wei
408-
Rbtc *entities.Wei
418+
// withdrawContractFunds retrieves the LP's balance from the pegin contract and withdraws it
419+
// back to the hot wallet, ensuring contract funds are prioritized before cold wallet transfers.
420+
func (useCase *TransferExcessToColdWalletUseCase) withdrawContractFunds() {
421+
lpAddress := useCase.generalProvider.RskAddress()
422+
contractBalance, err := useCase.contracts.PegIn.GetBalance(lpAddress)
423+
if err != nil {
424+
log.Errorf("TransferExcessToColdWallet: failed to get pegin contract balance: %v", err)
425+
return
426+
}
427+
if contractBalance.Cmp(entities.NewWei(0)) <= 0 {
428+
return
429+
}
430+
log.Infof("TransferExcessToColdWallet: withdrawing %s wei from pegin contract", contractBalance.String())
431+
if err := useCase.contracts.PegIn.Withdraw(contractBalance); err != nil {
432+
log.Errorf("TransferExcessToColdWallet: failed to withdraw from pegin contract: %v", err)
433+
}
409434
}
410435

411436
// validateColdWallet checks that both BTC and RSK cold wallet addresses are configured.

0 commit comments

Comments
 (0)