diff --git a/bridges/ethMultiversX/interface.go b/bridges/ethMultiversX/interface.go index 9e459101..d514d171 100644 --- a/bridges/ethMultiversX/interface.go +++ b/bridges/ethMultiversX/interface.go @@ -24,6 +24,7 @@ type MultiversXClient interface { GetActionIDForSetStatusOnPendingTransfer(ctx context.Context, batch *bridgeCore.TransferBatch) (uint64, error) GetLastExecutedEthBatchID(ctx context.Context) (uint64, error) GetLastExecutedEthTxID(ctx context.Context) (uint64, error) + GetLastMvxBatchID(ctx context.Context) (uint64, error) GetCurrentNonce(ctx context.Context) (uint64, error) ProposeSetStatus(ctx context.Context, batch *bridgeCore.TransferBatch) (string, error) diff --git a/clients/balanceValidator/balanceValidator.go b/clients/balanceValidator/balanceValidator.go index a912365c..0824dc41 100644 --- a/clients/balanceValidator/balanceValidator.go +++ b/clients/balanceValidator/balanceValidator.go @@ -112,8 +112,6 @@ func (validator *balanceValidator) CheckToken(ctx context.Context, ethToken comm "amount", amount.String(), ) - // TODO(next PRs): fix here to not consider the pending batch in the mvx->eth direction that executed on eth. - if ethAmount.Cmp(mvxAmount) != 0 { return fmt.Errorf("%w, balance for ERC20 token %s is %s and the balance for ESDT token %s is %s, direction %s", ErrBalanceMismatch, ethToken.String(), ethAmount.String(), mvxToken, mvxAmount.String(), direction) @@ -271,21 +269,13 @@ func getTotalAmountFromBatch(batch *bridgeCore.TransferBatch, token []byte) *big } func (validator *balanceValidator) getTotalTransferAmountInPendingMvxBatches(ctx context.Context, mvxToken []byte) (*big.Int, error) { - batch, err := validator.multiversXClient.GetPendingBatch(ctx) - if errors.Is(err, clients.ErrNoPendingBatchAvailable) { - return big.NewInt(0), nil - } + batchID, err := validator.multiversXClient.GetLastMvxBatchID(ctx) if err != nil { return nil, err } - // check if the pending batch is executed on Ethereum and is not final + var batch *bridgeCore.TransferBatch amount := big.NewInt(0) - if validator.batchExecutedAndNotFinalOnEth(ctx, batch.ID) { - amount.Add(amount, getTotalAmountFromBatch(batch, mvxToken)) - } - - batchID := batch.ID + 1 for { batch, err = validator.multiversXClient.GetBatch(ctx, batchID) if errors.Is(err, clients.ErrNoBatchAvailable) { @@ -295,18 +285,20 @@ func (validator *balanceValidator) getTotalTransferAmountInPendingMvxBatches(ctx return nil, err } + wasExecuted, errWasExecuted := validator.ethereumClient.WasExecuted(ctx, batch.ID) + if errWasExecuted != nil { + return nil, errWasExecuted + } + if wasExecuted { + return amount, nil + } + amountFromBatch := getTotalAmountFromBatch(batch, mvxToken) amount.Add(amount, amountFromBatch) - batchID++ + batchID-- // go to the previous batch } } -func (validator *balanceValidator) batchExecutedAndNotFinalOnEth(ctx context.Context, nonce uint64) bool { - // TODO: analyze if we need to check the statuses returned - _, err := validator.ethereumClient.GetTransactionsStatuses(ctx, nonce) - return err != nil -} - func (validator *balanceValidator) getTotalTransferAmountInPendingEthBatches(ctx context.Context, ethToken common.Address) (*big.Int, error) { batchID, err := validator.multiversXClient.GetLastExecutedEthBatchID(ctx) if err != nil { diff --git a/clients/balanceValidator/balanceValidator_test.go b/clients/balanceValidator/balanceValidator_test.go index d22c7f57..2f823872 100644 --- a/clients/balanceValidator/balanceValidator_test.go +++ b/clients/balanceValidator/balanceValidator_test.go @@ -36,7 +36,6 @@ type testConfiguration struct { totalBalancesOnEth *big.Int burnBalancesOnEth *big.Int mintBalancesOnEth *big.Int - isFinalOnEth bool isNativeOnMvx bool isMintBurnOnMvx bool @@ -61,7 +60,6 @@ func (cfg *testConfiguration) deepClone() testConfiguration { result := testConfiguration{ isNativeOnEth: cfg.isNativeOnEth, isMintBurnOnEth: cfg.isMintBurnOnEth, - isFinalOnEth: cfg.isFinalOnEth, isNativeOnMvx: cfg.isNativeOnMvx, isMintBurnOnMvx: cfg.isMintBurnOnMvx, errorsOnCalls: make(map[string]error), @@ -179,8 +177,7 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { t.Parallel() cfg := testConfiguration{ - direction: "", - isFinalOnEth: true, + direction: "", } result := validatorTester(cfg) assert.ErrorIs(t, result.error, ErrInvalidDirection) @@ -190,8 +187,7 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { t.Run("on isMintBurnOnEthereum", func(t *testing.T) { cfg := testConfiguration{ - direction: batchProcessor.FromMultiversX, - isFinalOnEth: true, + direction: batchProcessor.FromMultiversX, errorsOnCalls: map[string]error{ "MintBurnTokensEth": expectedError, }, @@ -203,8 +199,7 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { }) t.Run("on isMintBurnOnMultiversX", func(t *testing.T) { cfg := testConfiguration{ - direction: batchProcessor.ToMultiversX, - isFinalOnEth: true, + direction: batchProcessor.ToMultiversX, errorsOnCalls: map[string]error{ "IsMintBurnTokenMvx": expectedError, }, @@ -216,8 +211,7 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { }) t.Run("on isNativeOnEthereum", func(t *testing.T) { cfg := testConfiguration{ - direction: batchProcessor.ToMultiversX, - isFinalOnEth: true, + direction: batchProcessor.ToMultiversX, errorsOnCalls: map[string]error{ "NativeTokensEth": expectedError, }, @@ -229,8 +223,7 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { }) t.Run("on isNativeOnMultiversX", func(t *testing.T) { cfg := testConfiguration{ - direction: batchProcessor.FromMultiversX, - isFinalOnEth: true, + direction: batchProcessor.FromMultiversX, errorsOnCalls: map[string]error{ "IsNativeTokenMvx": expectedError, }, @@ -245,7 +238,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "TotalBalancesEth": expectedError, }, @@ -260,7 +252,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isNativeOnMvx: true, isMintBurnOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "BurnBalancesEth": expectedError, }, @@ -275,7 +266,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isNativeOnMvx: true, isMintBurnOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "MintBalancesEth": expectedError, }, @@ -290,7 +280,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isNativeOnMvx: true, isMintBurnOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "GetLastExecutedEthBatchIDMvx": expectedError, }, @@ -305,7 +294,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isNativeOnMvx: true, isMintBurnOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "GetBatchEth": expectedError, }, @@ -320,7 +308,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isNativeOnMvx: true, isMintBurnOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "TotalBalancesMvx": expectedError, }, @@ -335,7 +322,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "BurnBalancesMvx": expectedError, }, @@ -350,7 +336,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "MintBalancesMvx": expectedError, }, @@ -360,14 +345,13 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { assert.False(t, result.checkRequiredBalanceOnEthCalled) assert.True(t, result.checkRequiredBalanceOnMvxCalled) }) - t.Run("on computeMvxAmount, GetPendingBatch", func(t *testing.T) { + t.Run("on computeMvxAmount, GetLastMvxBatchID", func(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ - "GetPendingBatchMvx": expectedError, + "GetLastMvxBatchID": expectedError, }, } result := validatorTester(cfg) @@ -380,7 +364,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, errorsOnCalls: map[string]error{ "GetBatchMvx": expectedError, }, @@ -390,6 +373,20 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { assert.False(t, result.checkRequiredBalanceOnEthCalled) assert.True(t, result.checkRequiredBalanceOnMvxCalled) }) + t.Run("on computeMvxAmount, WasExecuted", func(t *testing.T) { + cfg := testConfiguration{ + direction: batchProcessor.ToMultiversX, + isMintBurnOnMvx: true, + isNativeOnEth: true, + errorsOnCalls: map[string]error{ + "WasExecutedEth": expectedError, + }, + } + result := validatorTester(cfg) + assert.Equal(t, expectedError, result.error) + assert.False(t, result.checkRequiredBalanceOnEthCalled) + assert.True(t, result.checkRequiredBalanceOnMvxCalled) + }) }) t.Run("invalid setup", func(t *testing.T) { t.Parallel() @@ -398,7 +395,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, - isFinalOnEth: true, } result := validatorTester(cfg) assert.ErrorIs(t, result.error, ErrInvalidSetup) @@ -410,7 +406,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isNativeOnEth: true, - isFinalOnEth: true, } result := validatorTester(cfg) assert.ErrorIs(t, result.error, ErrInvalidSetup) @@ -422,7 +417,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isNativeOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, } result := validatorTester(cfg) @@ -442,7 +436,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, isNativeOnEth: true, - isFinalOnEth: true, isMintBurnOnMvx: true, burnBalancesOnEth: big.NewInt(37), mintBalancesOnEth: big.NewInt(38), @@ -459,7 +452,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, burnBalancesOnEth: big.NewInt(38), mintBalancesOnEth: big.NewInt(37), @@ -476,7 +468,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isMintBurnOnMvx: true, isNativeOnMvx: true, burnBalancesOnMvx: big.NewInt(37), @@ -494,7 +485,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isNativeOnEth: true, - isFinalOnEth: true, isMintBurnOnMvx: true, burnBalancesOnMvx: big.NewInt(38), mintBalancesOnMvx: big.NewInt(37), @@ -518,7 +508,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, burnBalancesOnEth: big.NewInt(1100), // initial burn (1000) + burn from this transfer (100) mintBalancesOnEth: big.NewInt(11000), // minted (10000) + initial burn (1000) @@ -548,7 +537,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, burnBalancesOnEth: big.NewInt(1220), // initial burn (1000) + burn from this transfer (100) + burn from next batches (120) mintBalancesOnEth: big.NewInt(11000), // minted (10000) + initial burn (1000) @@ -580,7 +568,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, isMintBurnOnMvx: true, burnBalancesOnEth: big.NewInt(1100), // initial burn (1000) + burn from this transfer (100) @@ -612,7 +599,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, isMintBurnOnMvx: true, burnBalancesOnEth: big.NewInt(1220), // initial burn (1000) + burn from this transfer (100) + next batches (120) @@ -647,7 +633,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1000), // initial burn (1000) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) totalBalancesOnEth: big.NewInt(10100), // initial (10000) + locked from this transfer (100) @@ -677,7 +662,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1000), // initial burn (1000) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) totalBalancesOnEth: big.NewInt(10220), // initial (10000) + locked from this transfer (100) + next batches (120) @@ -710,7 +694,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnMvx: true, isNativeOnEth: true, isMintBurnOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1000), // initial burn (1000) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) burnBalancesOnEth: big.NewInt(12100), @@ -742,7 +725,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnMvx: true, isNativeOnEth: true, isMintBurnOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1000), // initial burn (1000) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) burnBalancesOnEth: big.NewInt(12220), @@ -779,7 +761,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.FromMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, burnBalancesOnEth: big.NewInt(1000), // initial burn (1000) mintBalancesOnEth: big.NewInt(11000), // minted (10000) + initial burn (1000) @@ -809,7 +790,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.FromMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, burnBalancesOnEth: big.NewInt(1000), // initial burn (1000) mintBalancesOnEth: big.NewInt(11000), // minted (10000) + initial burn (1000) @@ -841,7 +821,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.FromMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, isMintBurnOnMvx: true, burnBalancesOnEth: big.NewInt(1000), // initial burn (1000) @@ -873,7 +852,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { cfg := testConfiguration{ direction: batchProcessor.FromMultiversX, isMintBurnOnEth: true, - isFinalOnEth: true, isNativeOnMvx: true, isMintBurnOnMvx: true, burnBalancesOnEth: big.NewInt(1000), // initial burn (1000) @@ -908,7 +886,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1100), // initial burn (1000) + transfer from this batch (100) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) totalBalancesOnEth: big.NewInt(10000), // initial (10000) @@ -938,7 +915,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1220), // initial burn (1000) + transfer from this batch (100) + next batches (120) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) totalBalancesOnEth: big.NewInt(10000), // initial (10000) @@ -971,7 +947,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnMvx: true, isNativeOnEth: true, isMintBurnOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1100), // initial burn (1000) + transfer from this batch (100) mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) burnBalancesOnEth: big.NewInt(12000), @@ -1003,7 +978,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnMvx: true, isNativeOnEth: true, isMintBurnOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(1220), // initial burn (1000) + transfer from this batch (100) + transfer from next batches mintBalancesOnMvx: big.NewInt(11000), // minted (10000) + initial burn (1000) burnBalancesOnEth: big.NewInt(12000), @@ -1045,10 +1019,9 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnEth: true, isNativeOnMvx: true, - isFinalOnEth: true, burnBalancesOnEth: big.NewInt(existingBurnEth + 100 + 30 + 40 + 50), mintBalancesOnEth: big.NewInt(existingMintEth), - totalBalancesOnMvx: big.NewInt(existingNativeBalanceMvx + 60 + 80 + 100), // the current pending batch value (amount2) is not accounted + totalBalancesOnMvx: big.NewInt(existingNativeBalanceMvx + 60 + 80 + 100 + 200), amount: amount, pendingMvxBatchId: 1, amountsOnEthPendingBatches: map[uint64][]*big.Int{ @@ -1076,15 +1049,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.totalBalancesOnMvx.Add(copiedCfg.totalBalancesOnMvx, amount2) // the current pending batch value (amount2) should be accounted - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.False(t, result.checkRequiredBalanceOnEthCalled) - assert.True(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from Eth: native on MvX but with mint-burn, mint-burn on Eth, ok values, with next pending batches", func(t *testing.T) { t.Parallel() @@ -1099,10 +1063,9 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnEth: true, isNativeOnMvx: true, isMintBurnOnMvx: true, - isFinalOnEth: true, burnBalancesOnEth: big.NewInt(existingBurnEth + 100 + 30 + 40 + 50), mintBalancesOnEth: big.NewInt(existingMintEth), - burnBalancesOnMvx: big.NewInt(existingBurnMvx + 60 + 80 + 100), // the current pending batch value (amount2) is not accounted + burnBalancesOnMvx: big.NewInt(existingBurnMvx + 60 + 80 + 100 + 200), mintBalancesOnMvx: big.NewInt(existingMintMvx), amount: amount, pendingMvxBatchId: 1, @@ -1131,15 +1094,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.burnBalancesOnMvx.Add(copiedCfg.burnBalancesOnMvx, amount2) // the current pending batch value (amount2) should be accounted - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.False(t, result.checkRequiredBalanceOnEthCalled) - assert.True(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from Eth: native on Eth, mint-burn on MvX, ok values, with next pending batches", func(t *testing.T) { t.Parallel() @@ -1152,10 +1106,9 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.ToMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(existingBurnMvx + 200 + 60 + 80 + 100), mintBalancesOnMvx: big.NewInt(existingMintMvx), - totalBalancesOnEth: big.NewInt(existingNativeBalanceEth + 100 + 30 + 40 + 50 - 200), // initial + locked from this transfer + next batches - amount2 + totalBalancesOnEth: big.NewInt(existingNativeBalanceEth + 100 + 30 + 40 + 50), amount: amount, pendingMvxBatchId: 1, amountsOnEthPendingBatches: map[uint64][]*big.Int{ @@ -1183,15 +1136,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.totalBalancesOnEth = big.NewInt(existingNativeBalanceEth + 100 + 30 + 40 + 50) // the current pending batch value (amount2) should be accounted - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.False(t, result.checkRequiredBalanceOnEthCalled) - assert.True(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from Eth: native on Eth but with mint-burn, mint-burn on MvX, ok values, with next pending batches", func(t *testing.T) { t.Parallel() @@ -1206,11 +1150,10 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnMvx: true, isNativeOnEth: true, isMintBurnOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(existingBurnMvx + 200 + 60 + 80 + 100), mintBalancesOnMvx: big.NewInt(existingMintMvx), burnBalancesOnEth: big.NewInt(existingBurnEth + 100 + 30 + 40 + 50), - mintBalancesOnEth: big.NewInt(existingMintEth + 200), // we should add amount 2 here + mintBalancesOnEth: big.NewInt(existingMintEth), amount: amount, pendingMvxBatchId: 1, amountsOnEthPendingBatches: map[uint64][]*big.Int{ @@ -1238,15 +1181,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.mintBalancesOnEth = big.NewInt(existingMintEth) // the burn balance was not yet updated with amount2 - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.False(t, result.checkRequiredBalanceOnEthCalled) - assert.True(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from MvX: native on MvX, mint-burn on Eth, ok values, with next pending batches on both chains", func(t *testing.T) { t.Parallel() @@ -1259,10 +1193,9 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isMintBurnOnEth: true, isNativeOnMvx: true, - isFinalOnEth: true, burnBalancesOnEth: big.NewInt(existingBurnEth + 200 + 60 + 80 + 100), mintBalancesOnEth: big.NewInt(existingMintEth), - totalBalancesOnMvx: big.NewInt(existingNativeBalanceMvx + 30 + 40 + 50), // the current pending batch value (amount) is not accounted + totalBalancesOnMvx: big.NewInt(existingNativeBalanceMvx + 30 + 40 + 50 + 100), amount: amount, pendingMvxBatchId: 1, amountsOnMvxPendingBatches: map[uint64][]*big.Int{ @@ -1290,15 +1223,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.totalBalancesOnMvx.Add(copiedCfg.totalBalancesOnMvx, amount) // the current pending batch value (amount) should be accounted - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.True(t, result.checkRequiredBalanceOnEthCalled) - assert.False(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from MvX: native on MvX but with mint-burn, mint-burn on Eth, ok values, with next pending batches", func(t *testing.T) { t.Parallel() @@ -1313,10 +1237,9 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnEth: true, isNativeOnMvx: true, isMintBurnOnMvx: true, - isFinalOnEth: true, burnBalancesOnEth: big.NewInt(existingBurnEth + 200 + 60 + 80 + 100), mintBalancesOnEth: big.NewInt(existingMintEth), - burnBalancesOnMvx: big.NewInt(existingBurnMvx + 30 + 40 + 50), // the current pending batch value (amount) is not accounted + burnBalancesOnMvx: big.NewInt(existingBurnMvx + 30 + 40 + 50 + 100), mintBalancesOnMvx: big.NewInt(existingMintMvx), amount: amount, pendingMvxBatchId: 1, @@ -1345,15 +1268,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.burnBalancesOnMvx.Add(copiedCfg.burnBalancesOnMvx, amount) // the current pending batch value (amount) should be accounted - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.True(t, result.checkRequiredBalanceOnEthCalled) - assert.False(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from MvX: native on Eth, mint-burn on MvX, ok values, with next pending batches", func(t *testing.T) { t.Parallel() @@ -1366,10 +1280,9 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { direction: batchProcessor.FromMultiversX, isMintBurnOnMvx: true, isNativeOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(existingBurnMvx + 100 + 30 + 40 + 50), mintBalancesOnMvx: big.NewInt(existingMintMvx), - totalBalancesOnEth: big.NewInt(existingNativeBalanceEth + 200 + 60 + 80 + 100 - 100), // initial + locked from this transfer + next batches - amount + totalBalancesOnEth: big.NewInt(existingNativeBalanceEth + 200 + 60 + 80 + 100), amount: amount, pendingMvxBatchId: 1, amountsOnMvxPendingBatches: map[uint64][]*big.Int{ @@ -1397,15 +1310,6 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.totalBalancesOnEth = big.NewInt(existingNativeBalanceEth + 200 + 60 + 80 + 100) // the current pending batch value (amount) should be accounted - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.True(t, result.checkRequiredBalanceOnEthCalled) - assert.False(t, result.checkRequiredBalanceOnMvxCalled) - }) }) t.Run("from MvX: native on Eth but with mint-burn, mint-burn on MvX, ok values, with next pending batches", func(t *testing.T) { t.Parallel() @@ -1420,11 +1324,10 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { isMintBurnOnMvx: true, isNativeOnEth: true, isMintBurnOnEth: true, - isFinalOnEth: true, burnBalancesOnMvx: big.NewInt(existingBurnMvx + 100 + 30 + 40 + 50), mintBalancesOnMvx: big.NewInt(existingMintMvx), burnBalancesOnEth: big.NewInt(existingBurnEth + 200 + 60 + 80 + 100), - mintBalancesOnEth: big.NewInt(existingMintEth + 100), // we should add amount here + mintBalancesOnEth: big.NewInt(existingMintEth), amount: amount, pendingMvxBatchId: 1, amountsOnMvxPendingBatches: map[uint64][]*big.Int{ @@ -1452,18 +1355,8 @@ func TestBridgeExecutor_CheckToken(t *testing.T) { result = validatorTester(copiedCfg) assert.ErrorIs(t, result.error, ErrBalanceMismatch) }) - t.Run("non final batch on eth", func(t *testing.T) { - copiedCfg := cfg.deepClone() - copiedCfg.isFinalOnEth = false - copiedCfg.mintBalancesOnEth = big.NewInt(existingMintEth) // the burn balance was not yet updated with amount - result = validatorTester(copiedCfg) - assert.Nil(t, result.error) - assert.True(t, result.checkRequiredBalanceOnEthCalled) - assert.False(t, result.checkRequiredBalanceOnMvxCalled) - }) }) }) - }) } @@ -1472,6 +1365,13 @@ func validatorTester(cfg testConfiguration) testResult { result := testResult{} + lastMvxBatchID := uint64(0) + for key := range cfg.amountsOnMvxPendingBatches { + if key > lastMvxBatchID { + lastMvxBatchID = key + } + } + args.MultiversXClient = &bridge.MultiversXClientStub{ CheckRequiredBalanceCalled: func(ctx context.Context, token []byte, value *big.Int) error { result.checkRequiredBalanceOnMvxCalled = true @@ -1554,6 +1454,14 @@ func validatorTester(cfg testConfiguration) testResult { return cfg.lastExecutedEthBatch, nil }, + GetLastMvxBatchIDCalled: func(ctx context.Context) (uint64, error) { + err := cfg.errorsOnCalls["GetLastMvxBatchID"] + if err != nil { + return 0, err + } + + return lastMvxBatchID, nil + }, } args.EthereumClient = &bridge.EthereumClientStub{ CheckRequiredBalanceCalled: func(ctx context.Context, erc20Address common.Address, value *big.Int) error { @@ -1613,12 +1521,14 @@ func validatorTester(cfg testConfiguration) testResult { return batch, false, nil }, - GetTransactionsStatusesCalled: func(ctx context.Context, batchId uint64) ([]byte, error) { - if cfg.isFinalOnEth { - return make([]byte, 0), nil + WasExecutedCalled: func(ctx context.Context, batchID uint64) (bool, error) { + err := cfg.errorsOnCalls["WasExecutedEth"] + if err != nil { + return false, err } - return nil, errors.New("not a final batch") + _, found := cfg.amountsOnMvxPendingBatches[batchID] + return !found, nil }, } diff --git a/clients/balanceValidator/interface.go b/clients/balanceValidator/interface.go index 272e19d5..7e8c0388 100644 --- a/clients/balanceValidator/interface.go +++ b/clients/balanceValidator/interface.go @@ -13,6 +13,7 @@ type MultiversXClient interface { GetPendingBatch(ctx context.Context) (*bridgeCore.TransferBatch, error) GetBatch(ctx context.Context, batchID uint64) (*bridgeCore.TransferBatch, error) GetLastExecutedEthBatchID(ctx context.Context) (uint64, error) + GetLastMvxBatchID(ctx context.Context) (uint64, error) IsMintBurnToken(ctx context.Context, token []byte) (bool, error) IsNativeToken(ctx context.Context, token []byte) (bool, error) TotalBalances(ctx context.Context, token []byte) (*big.Int, error) @@ -31,6 +32,6 @@ type EthereumClient interface { MintBurnTokens(ctx context.Context, token common.Address) (bool, error) NativeTokens(ctx context.Context, token common.Address) (bool, error) CheckRequiredBalance(ctx context.Context, erc20Address common.Address, value *big.Int) error - GetTransactionsStatuses(ctx context.Context, batchId uint64) ([]byte, error) + WasExecuted(ctx context.Context, mvxBatchID uint64) (bool, error) IsInterfaceNil() bool } diff --git a/clients/ethereum/client.go b/clients/ethereum/client.go index 2268b42d..fc45358f 100644 --- a/clients/ethereum/client.go +++ b/clients/ethereum/client.go @@ -239,9 +239,9 @@ func (c *client) GetBatchSCMetadata(ctx context.Context, nonce uint64, blockNumb return depositEvents, nil } -// WasExecuted returns true if the batch ID was executed -func (c *client) WasExecuted(ctx context.Context, batchID uint64) (bool, error) { - return c.clientWrapper.WasBatchExecuted(ctx, big.NewInt(0).SetUint64(batchID)) +// WasExecuted returns true if the MultiversX batch ID was executed +func (c *client) WasExecuted(ctx context.Context, mvxBatchID uint64) (bool, error) { + return c.clientWrapper.WasBatchExecuted(ctx, big.NewInt(0).SetUint64(mvxBatchID)) } // BroadcastSignatureForMessageHash will send the signature for the provided message hash diff --git a/clients/ethereum/erc20ContractsHolder.go b/clients/ethereum/erc20ContractsHolder.go index f07237b9..3eaf99c1 100644 --- a/clients/ethereum/erc20ContractsHolder.go +++ b/clients/ethereum/erc20ContractsHolder.go @@ -45,8 +45,17 @@ func NewErc20SafeContractsHolder(args ArgsErc20SafeContractsHolder) (*erc20SafeC } // BalanceOf returns the ERC20 balance of the provided address -// if the ERC20 contract does not exists in the map of contract wrappers, it will create and add it first +// if the ERC20 contract does not exist in the map of contract wrappers, it will create and add it first func (h *erc20SafeContractsHolder) BalanceOf(ctx context.Context, erc20Address ethCommon.Address, address ethCommon.Address) (*big.Int, error) { + wrapper, err := h.getOrCreateWrapper(erc20Address) + if err != nil { + return nil, err + } + + return wrapper.BalanceOf(ctx, address) +} + +func (h *erc20SafeContractsHolder) getOrCreateWrapper(erc20Address ethCommon.Address) (erc20ContractWrapper, error) { h.mut.Lock() defer h.mut.Unlock() @@ -68,7 +77,18 @@ func (h *erc20SafeContractsHolder) BalanceOf(ctx context.Context, erc20Address e h.contracts[erc20Address] = wrapper } - return wrapper.BalanceOf(ctx, address) + return wrapper, nil +} + +// Decimals returns the ERC20 decimals for the current ERC20 contract +// if the ERC20 contract does not exist in the map of contract wrappers, it will create and add it first +func (h *erc20SafeContractsHolder) Decimals(ctx context.Context, erc20Address ethCommon.Address) (uint8, error) { + wrapper, err := h.getOrCreateWrapper(erc20Address) + if err != nil { + return 0, err + } + + return wrapper.Decimals(ctx) } // IsInterfaceNil returns true if there is no value under the interface diff --git a/clients/ethereum/erc20ContractsHolder_test.go b/clients/ethereum/erc20ContractsHolder_test.go index 81ccc011..e1858ce7 100644 --- a/clients/ethereum/erc20ContractsHolder_test.go +++ b/clients/ethereum/erc20ContractsHolder_test.go @@ -53,10 +53,10 @@ func TestNewErc20SafeContractsHolder(t *testing.T) { }) } -func TestBalanceOf(t *testing.T) { +func TestErc20SafeContractsHolder_BalanceOf(t *testing.T) { t.Parallel() - t.Run("address does not exists on map nor blockchain", func(t *testing.T) { + t.Run("address does not exist on map nor blockchain", func(t *testing.T) { expectedError := errors.New("no contract code at given address") args := createMockArgsContractsHolder() args.EthClient = &bridgeTests.ContractBackendStub{ @@ -109,7 +109,7 @@ func TestBalanceOf(t *testing.T) { assert.Equal(t, 0, len(ch.contracts)) result, err := ch.BalanceOf(context.Background(), contractAddress, address1) - // first time the contract does not exists in the map, so it should add it + // first time the contract does not exist in the map, so it should add it assert.Nil(t, err) assert.Equal(t, big.NewInt(returnedBalance), result) assert.Equal(t, 1, len(ch.contracts)) @@ -121,7 +121,7 @@ func TestBalanceOf(t *testing.T) { assert.Equal(t, 1, len(ch.contracts)) }) - t.Run("new contract addres while another contracts already exists", func(t *testing.T) { + t.Run("new contract address while another contracts already exists", func(t *testing.T) { var returnedBalance int64 = 1000 args := createMockArgsContractsHolder() args.EthClient = &bridgeTests.ContractBackendStub{ @@ -160,6 +160,108 @@ func TestBalanceOf(t *testing.T) { }) } +func TestErc20SafeContractsHolder_Decimals(t *testing.T) { + t.Parallel() + + t.Run("address does not exist on map nor blockchain", func(t *testing.T) { + expectedError := errors.New("no contract code at given address") + args := createMockArgsContractsHolder() + args.EthClient = &bridgeTests.ContractBackendStub{ + CallContractCalled: func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return nil, expectedError + }, + } + ch, err := NewErc20SafeContractsHolder(args) + assert.Nil(t, err) + assert.False(t, check.IfNil(ch)) + assert.Equal(t, 0, len(ch.contracts)) + + result, err := ch.Decimals(context.Background(), testsCommon.CreateRandomEthereumAddress()) + assert.Equal(t, expectedError, err) + assert.Zero(t, result) + assert.Equal(t, 1, len(ch.contracts)) + }) + t.Run("address exists only on blockchain", func(t *testing.T) { + returnedDecimals := byte(37) + args := createMockArgsContractsHolder() + args.EthClient = &bridgeTests.ContractBackendStub{ + CallContractCalled: func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return convertByteValueToByteSlice(returnedDecimals), nil + }, + } + ch, err := NewErc20SafeContractsHolder(args) + assert.Nil(t, err) + assert.False(t, check.IfNil(ch)) + assert.Equal(t, 0, len(ch.contracts)) + + result, err := ch.Decimals(context.Background(), testsCommon.CreateRandomEthereumAddress()) + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 1, len(ch.contracts)) + }) + t.Run("address exists also in contracts map", func(t *testing.T) { + returnedDecimals := byte(38) + args := createMockArgsContractsHolder() + args.EthClient = &bridgeTests.ContractBackendStub{ + CallContractCalled: func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return convertByteValueToByteSlice(returnedDecimals), nil + }, + } + ch, err := NewErc20SafeContractsHolder(args) + contractAddress := testsCommon.CreateRandomEthereumAddress() + assert.Nil(t, err) + assert.False(t, check.IfNil(ch)) + assert.Equal(t, 0, len(ch.contracts)) + + result, err := ch.Decimals(context.Background(), contractAddress) + // first time the contract does not exist in the map, so it should add it + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 1, len(ch.contracts)) + + result, err = ch.Decimals(context.Background(), contractAddress) + // second time the contract already exists in the map, so it should just use it + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 1, len(ch.contracts)) + }) + t.Run("new contract address while another contracts already exists", func(t *testing.T) { + returnedDecimals := byte(39) + args := createMockArgsContractsHolder() + args.EthClient = &bridgeTests.ContractBackendStub{ + CallContractCalled: func(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return convertByteValueToByteSlice(returnedDecimals), nil + }, + } + ch, err := NewErc20SafeContractsHolder(args) + contractAddress1 := testsCommon.CreateRandomEthereumAddress() + contractAddress2 := testsCommon.CreateRandomEthereumAddress() + assert.Nil(t, err) + assert.False(t, check.IfNil(ch)) + assert.Equal(t, 0, len(ch.contracts)) + + result, err := ch.Decimals(context.Background(), contractAddress1) + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 1, len(ch.contracts)) + + result, err = ch.Decimals(context.Background(), contractAddress1) + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 1, len(ch.contracts)) + + result, err = ch.Decimals(context.Background(), contractAddress2) + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 2, len(ch.contracts)) + + result, err = ch.Decimals(context.Background(), contractAddress2) + assert.Nil(t, err) + assert.Equal(t, returnedDecimals, result) + assert.Equal(t, 2, len(ch.contracts)) + }) +} + func convertBigToAbiCompatible(number *big.Int) []byte { numberAsBytes := number.Bytes() size := len(numberAsBytes) @@ -170,3 +272,10 @@ func convertBigToAbiCompatible(number *big.Int) []byte { } return bs } + +func convertByteValueToByteSlice(value byte) []byte { + result := make([]byte, 32) + result[len(result)-1] = value + + return result +} diff --git a/clients/ethereum/interface.go b/clients/ethereum/interface.go index d6a2ca9d..ed96a62b 100644 --- a/clients/ethereum/interface.go +++ b/clients/ethereum/interface.go @@ -41,6 +41,7 @@ type ClientWrapper interface { // Erc20ContractsHolder defines the Ethereum ERC20 contract operations type Erc20ContractsHolder interface { BalanceOf(ctx context.Context, erc20Address common.Address, address common.Address) (*big.Int, error) + Decimals(ctx context.Context, erc20Address common.Address) (uint8, error) IsInterfaceNil() bool } @@ -71,6 +72,7 @@ type SignaturesHolder interface { type erc20ContractWrapper interface { BalanceOf(ctx context.Context, account common.Address) (*big.Int, error) + Decimals(ctx context.Context) (uint8, error) IsInterfaceNil() bool } diff --git a/clients/ethereum/wrappers/erc20ContractWrapper.go b/clients/ethereum/wrappers/erc20ContractWrapper.go index 9ea12776..ef4f55b2 100644 --- a/clients/ethereum/wrappers/erc20ContractWrapper.go +++ b/clients/ethereum/wrappers/erc20ContractWrapper.go @@ -43,6 +43,12 @@ func (wrapper *erc20ContractWrapper) BalanceOf(ctx context.Context, account comm return wrapper.erc20Contract.BalanceOf(&bind.CallOpts{Context: ctx}, account) } +// Decimals returns the ERC20 set decimals for the token +func (wrapper *erc20ContractWrapper) Decimals(ctx context.Context) (uint8, error) { + wrapper.statusHandler.AddIntMetric(core.MetricNumEthClientRequests, 1) + return wrapper.erc20Contract.Decimals(&bind.CallOpts{Context: ctx}) +} + // IsInterfaceNil returns true if there is no value under the interface func (wrapper *erc20ContractWrapper) IsInterfaceNil() bool { return wrapper == nil diff --git a/clients/ethereum/wrappers/erc20ContractWrapper_test.go b/clients/ethereum/wrappers/erc20ContractWrapper_test.go index c51f8e4f..9d28fbea 100644 --- a/clients/ethereum/wrappers/erc20ContractWrapper_test.go +++ b/clients/ethereum/wrappers/erc20ContractWrapper_test.go @@ -69,3 +69,22 @@ func TestErc20ContractWrapper_BalanceOf(t *testing.T) { assert.True(t, handlerCalled) assert.Equal(t, 1, statusHandler.GetIntMetric(core.MetricNumEthClientRequests)) } + +func TestErc20ContractWrapper_Decimals(t *testing.T) { + t.Parallel() + + args, statusHandler := createMockArgsErc20ContractWrapper() + handlerCalled := false + args.Erc20Contract = &interactors.GenericErc20ContractStub{ + DecimalsCalled: func() (uint8, error) { + handlerCalled = true + return 37, nil + }, + } + wrapper, _ := NewErc20ContractWrapper(args) + decimals, err := wrapper.Decimals(context.TODO()) + assert.Nil(t, err) + assert.Equal(t, byte(37), decimals) + assert.True(t, handlerCalled) + assert.Equal(t, 1, statusHandler.GetIntMetric(core.MetricNumEthClientRequests)) +} diff --git a/clients/ethereum/wrappers/interface.go b/clients/ethereum/wrappers/interface.go index bbe9ab2c..f6e09953 100644 --- a/clients/ethereum/wrappers/interface.go +++ b/clients/ethereum/wrappers/interface.go @@ -13,6 +13,7 @@ import ( type genericErc20Contract interface { BalanceOf(opts *bind.CallOpts, account common.Address) (*big.Int, error) + Decimals(opts *bind.CallOpts) (uint8, error) } type multiSigContract interface { diff --git a/clients/multiversx/mxClientDataGetter.go b/clients/multiversx/mxClientDataGetter.go index 324f03ea..d1f78491 100644 --- a/clients/multiversx/mxClientDataGetter.go +++ b/clients/multiversx/mxClientDataGetter.go @@ -42,6 +42,7 @@ const ( getMintBalances = "getMintBalances" getBurnBalances = "getBurnBalances" getAllKnownTokens = "getAllKnownTokens" + getLastBatchId = "getLastBatchId" ) // ArgsMXClientDataGetter is the arguments DTO used in the NewMXClientDataGetter constructor @@ -535,6 +536,14 @@ func (dataGetter *mxClientDataGetter) GetAllKnownTokens(ctx context.Context) ([] return dataGetter.executeQueryFromBuilder(ctx, builder) } +// GetLastMvxBatchID returns the highest batch ID the safe contract reached. This might be a WIP batch that is not executable yet +func (dataGetter *mxClientDataGetter) GetLastMvxBatchID(ctx context.Context) (uint64, error) { + builder := dataGetter.createSafeDefaultVmQueryBuilder() + builder.Function(getLastBatchId) + + return dataGetter.executeQueryUint64FromBuilder(ctx, builder) +} + // IsInterfaceNil returns true if there is no value under the interface func (dataGetter *mxClientDataGetter) IsInterfaceNil() bool { return dataGetter == nil diff --git a/clients/multiversx/mxClientDataGetter_test.go b/clients/multiversx/mxClientDataGetter_test.go index d21ea26b..2531acd2 100644 --- a/clients/multiversx/mxClientDataGetter_test.go +++ b/clients/multiversx/mxClientDataGetter_test.go @@ -1588,3 +1588,36 @@ func TestMultiversXClientDataGetter_getBurnBalances(t *testing.T) { assert.Equal(t, result, expectedAccumulatedBurnedTokens) assert.True(t, proxyCalled) } + +func TestMultiversXClientDataGetter_GetLastMvxBatchID(t *testing.T) { + t.Parallel() + + args := createMockArgsMXClientDataGetter() + proxyCalled := false + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + proxyCalled = true + assert.Equal(t, getBech32Address(args.SafeContractAddress), vmRequest.Address) + assert.Equal(t, getBech32Address(args.RelayerAddress), vmRequest.CallerAddr) + assert.Equal(t, "", vmRequest.CallValue) + assert.Equal(t, getLastBatchId, vmRequest.FuncName) + assert.Empty(t, vmRequest.Args) + + strResponse := "Dpk=" + response, _ := base64.StdEncoding.DecodeString(strResponse) + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: [][]byte{response}, + }, + }, nil + }, + } + + dg, _ := NewMXClientDataGetter(args) + + result, err := dg.GetLastMvxBatchID(context.Background()) + assert.Nil(t, err) + assert.Equal(t, uint64(3737), result) + assert.True(t, proxyCalled) +} diff --git a/cmd/migration/config/config-mainnet-bsc.toml b/cmd/migration/config/config-mainnet-bsc.toml new file mode 100644 index 00000000..186ba577 --- /dev/null +++ b/cmd/migration/config/config-mainnet-bsc.toml @@ -0,0 +1,36 @@ +[Eth] + Chain = "BSC" + NetworkAddress = "" # a network address + PrivateKeyFile = "keys/ethereum.sk" # the path to the file containing the relayer eth private key + MultisigContractAddress = "0xc58848bc00e6522C7Fd3F16a94BBb33b90549a12" + SafeContractAddress = "0x7334ba16020c1444957b75032165c0a6292ba09a" + GasLimitBase = 350000 + GasLimitForEach = 30000 + [Eth.GasStation] + Enabled = true + URL = "https://api.bscscan.com/api?module=gastracker&action=gasoracle" # gas station URL. Suggestion to provide the api-key here + GasPriceMultiplier = 1000000000 # the value to be multiplied with the fetched value. Useful in test chains. On production chain should be 1000000000 + PollingIntervalInSeconds = 60 # number of seconds between gas price polling + RequestRetryDelayInSeconds = 5 # number of seconds of delay after one failed request + MaxFetchRetries = 3 # number of fetch retries before printing an error + RequestTimeInSeconds = 5 # maximum timeout (in seconds) for the gas price request + MaximumAllowedGasPrice = 300 # maximum value allowed for the fetched gas price value + # GasPriceSelector available options: "SafeGasPrice", "ProposeGasPrice", "FastGasPrice" + GasPriceSelector = "SafeGasPrice" # selector used to provide the gas price + +[MultiversX] + NetworkAddress = "https://gateway.multiversx.com" # the network address + MultisigContractAddress = "erd1qqqqqqqqqqqqqpgq6pg5e8twgk6vvnzxzmvfyrphrfs3e3c3yfkqe6kx0x" + SafeContractAddress = "erd1qqqqqqqqqqqqqpgq2tf9zm9xxv96gz50p8jq5jesudhz45dvyfkqyefrss" + [MultiversX.Proxy] + CacherExpirationSeconds = 600 # the caching time in seconds + + # valid options for ProxyRestAPIEntityType are "observer" and "proxy". Any other value will trigger an error. + # "observer" is useful when querying an observer, directly and "proxy" is useful when querying a squad's proxy (gateway) + RestAPIEntityType = "proxy" + FinalityCheck = true + MaxNoncesDelta = 7 # the number of maximum blocks allowed to be "in front" of what the metachain has notarized + +[Logs] + LogFileLifeSpanInSec = 86400 # 24h + LogFileLifeSpanInMB = 1024 # 1GB diff --git a/cmd/migration/config/config-mainnet-eth.toml b/cmd/migration/config/config-mainnet-eth.toml new file mode 100644 index 00000000..a1a4d8ff --- /dev/null +++ b/cmd/migration/config/config-mainnet-eth.toml @@ -0,0 +1,36 @@ +[Eth] + Chain = "Ethereum" + NetworkAddress = "" # a network address + PrivateKeyFile = "keys/ethereum.sk" # the path to the file containing the relayer eth private key + MultisigContractAddress = "0x1Ff78EB04d44a803E73c44FEf8790c5cAbD14596" + SafeContractAddress = "0x92A26975433A61CF1134802586aa669bAB8B69f3" + GasLimitBase = 350000 + GasLimitForEach = 30000 + [Eth.GasStation] + Enabled = true + URL = "https://api.etherscan.io/api?module=gastracker&action=gasoracle" # gas station URL. Suggestion to provide the api-key here + GasPriceMultiplier = 1000000000 # the value to be multiplied with the fetched value. Useful in test chains. On production chain should be 1000000000 + PollingIntervalInSeconds = 60 # number of seconds between gas price polling + RequestRetryDelayInSeconds = 5 # number of seconds of delay after one failed request + MaxFetchRetries = 3 # number of fetch retries before printing an error + RequestTimeInSeconds = 5 # maximum timeout (in seconds) for the gas price request + MaximumAllowedGasPrice = 300 # maximum value allowed for the fetched gas price value + # GasPriceSelector available options: "SafeGasPrice", "ProposeGasPrice", "FastGasPrice" + GasPriceSelector = "SafeGasPrice" # selector used to provide the gas price + +[MultiversX] + NetworkAddress = "https://gateway.multiversx.com" # the network address + MultisigContractAddress = "erd1qqqqqqqqqqqqqpgqxexs26vrvhwh2m4he62d6y3jzmv3qkujyfkq8yh4z2" + SafeContractAddress = "erd1qqqqqqqqqqqqqpgqhxkc48lt5uv2hejj4wtjqvugfm4wgv6gyfkqw0uuxl" + [MultiversX.Proxy] + CacherExpirationSeconds = 600 # the caching time in seconds + + # valid options for ProxyRestAPIEntityType are "observer" and "proxy". Any other value will trigger an error. + # "observer" is useful when querying an observer, directly and "proxy" is useful when querying a squad's proxy (gateway) + RestAPIEntityType = "proxy" + FinalityCheck = true + MaxNoncesDelta = 7 # the number of maximum blocks allowed to be "in front" of what the metachain has notarized + +[Logs] + LogFileLifeSpanInSec = 86400 # 24h + LogFileLifeSpanInMB = 1024 # 1GB diff --git a/cmd/migration/config/config.toml b/cmd/migration/config/config.toml index 9958e13c..6f8923a2 100644 --- a/cmd/migration/config/config.toml +++ b/cmd/migration/config/config.toml @@ -2,8 +2,8 @@ Chain = "Ethereum" NetworkAddress = "http://127.0.0.1:8545" # a network address PrivateKeyFile = "keys/ethereum.sk" # the path to the file containing the relayer eth private key - MultisigContractAddress = "92266a070ae1eBA4F778781c2177AaF4747Ea1b8" - SafeContractAddress = "29C63343d302564e3695ca14AB31F5eec427Af3E" + MultisigContractAddress = "1Ff78EB04d44a803E73c44FEf8790c5cAbD14596" + SafeContractAddress = "92A26975433A61CF1134802586aa669bAB8B69f3" GasLimitBase = 350000 GasLimitForEach = 30000 [Eth.GasStation] @@ -19,9 +19,9 @@ GasPriceSelector = "SafeGasPrice" # selector used to provide the gas price [MultiversX] - NetworkAddress = "https://testnet-gateway.multiversx.com" # the network address - MultisigContractAddress = "erd1qqqqqqqqqqqqqpgqe34pfpl27yq9hq79kms9glwc6c8efm5u3kuq02d609" - SafeContractAddress = "erd1qqqqqqqqqqqqqpgq3quw8f6mplxn6up7l5wsre0dm8r9wrds3kuq7axccv" + NetworkAddress = "https://gateway.multiversx.com" # the network address + MultisigContractAddress = "erd1qqqqqqqqqqqqqpgqxexs26vrvhwh2m4he62d6y3jzmv3qkujyfkq8yh4z2" + SafeContractAddress = "erd1qqqqqqqqqqqqqpgqhxkc48lt5uv2hejj4wtjqvugfm4wgv6gyfkqw0uuxl" [MultiversX.Proxy] CacherExpirationSeconds = 600 # the caching time in seconds diff --git a/cmd/migration/flags.go b/cmd/migration/flags.go index 04768b53..e15c15a4 100644 --- a/cmd/migration/flags.go +++ b/cmd/migration/flags.go @@ -25,8 +25,8 @@ var ( } mode = cli.StringFlag{ Name: "mode", - Usage: "This flag specifies the operation mode. Usage: sign or execute", - Value: signMode, + Usage: "This flag specifies the operation mode. Usage: query, sign or execute", + Value: queryMode, } migrationJsonFile = cli.StringFlag{ Name: "migration-file", @@ -43,9 +43,12 @@ var ( Usage: "The new safe address on Ethereum", Value: "", } - denominatedAmount = cli.Uint64Flag{ - Name: "denominated-amount", - Usage: "The dominated amount that will be used on all deposits. Very useful in an initial test", + partialMigration = cli.StringFlag{ + Name: "partial-migration", + Usage: "If a partial migration is wanted, this option can be very handy. We can specify an unlimited tuples in a single string, like this: " + + "`-partial-migration token1:amount1,token2:amount2,token1:amount3` and so on. You can see that the same token can be specified multiple times, " + + "the amounts will be added. The amount should be specified as a denominated value (does not contain all decimals, the conversion will be done " + + "automatically by the tool). Real example: `-partial-migration token1:amount1,token2:amount2,token1:amount3`", } ) @@ -57,7 +60,7 @@ func getFlags() []cli.Flag { migrationJsonFile, signatureJsonFile, newSafeAddress, - denominatedAmount, + partialMigration, } } func getFlagsConfig(ctx *cli.Context) config.ContextFlagsConfig { diff --git a/cmd/migration/interface.go b/cmd/migration/interface.go new file mode 100644 index 00000000..086ccffc --- /dev/null +++ b/cmd/migration/interface.go @@ -0,0 +1,15 @@ +package main + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/multiversx/mx-bridge-eth-go/executors/ethereum" +) + +// BatchCreator defines the operations implemented by an entity that can create an Ethereum batch message that can be used +// in signing or transfer execution +type BatchCreator interface { + CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, partialMigration map[string]*big.Float) (*ethereum.BatchInfo, error) +} diff --git a/cmd/migration/main.go b/cmd/migration/main.go index 3bf92b7d..5ff13500 100644 --- a/cmd/migration/main.go +++ b/cmd/migration/main.go @@ -32,6 +32,7 @@ import ( const ( filePathPlaceholder = "[path]" + queryMode = "query" signMode = "sign" executeMode = "execute" configPath = "config" @@ -42,6 +43,7 @@ const ( var log = logger.GetOrCreate("main") type internalComponents struct { + creator BatchCreator batch *ethereum.BatchInfo cryptoHandler ethereumClient.CryptoHandler ethClient *ethclient.Client @@ -90,6 +92,8 @@ func execute(ctx *cli.Context) error { operationMode := strings.ToLower(ctx.GlobalString(mode.Name)) switch operationMode { + case queryMode: + return executeQuery(cfg) case signMode: _, err = generateAndSign(ctx, cfg) return err @@ -100,7 +104,27 @@ func execute(ctx *cli.Context) error { return fmt.Errorf("unknown execution mode: %s", operationMode) } -func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) (*internalComponents, error) { +func executeQuery(cfg config.MigrationToolConfig) error { + components, err := createInternalComponentsWithBatchCreator(cfg) + if err != nil { + return err + } + + dummyEthAddress := common.Address{} + info, err := components.creator.CreateBatchInfo(context.Background(), dummyEthAddress, nil) + if err != nil { + return err + } + + log.Info(fmt.Sprintf("Token balances for ERC20 safe address %s\n%s", + cfg.Eth.SafeContractAddress, + ethereum.TokensBalancesDisplayString(info), + )) + + return nil +} + +func createInternalComponentsWithBatchCreator(cfg config.MigrationToolConfig) (*internalComponents, error) { argsProxy := blockchain.ArgsProxy{ ProxyURL: cfg.MultiversX.NetworkAddress, SameScState: false, @@ -183,35 +207,49 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) (*interna return nil, err } + return &internalComponents{ + creator: creator, + ethClient: ethClient, + ethereumChainWrapper: ethereumChainWrapper, + }, nil +} + +func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) (*internalComponents, error) { + components, err := createInternalComponentsWithBatchCreator(cfg) + if err != nil { + return nil, err + } + newSafeAddressString := ctx.GlobalString(newSafeAddress.Name) if len(newSafeAddressString) == 0 { return nil, fmt.Errorf("invalid new safe address for Ethereum") } newSafeAddressValue := common.HexToAddress(ctx.GlobalString(newSafeAddress.Name)) - trimValue := chainCore.OptionalUint64{ - Value: ctx.GlobalUint64(denominatedAmount.Name), - HasValue: ctx.IsSet(denominatedAmount.Name), + partialMigration, err := ethereum.ConvertPartialMigrationStringToMap(ctx.GlobalString(partialMigration.Name)) + if err != nil { + return nil, err } - batchInfo, err := creator.CreateBatchInfo(context.Background(), newSafeAddressValue, trimValue) + + components.batch, err = components.creator.CreateBatchInfo(context.Background(), newSafeAddressValue, partialMigration) if err != nil { return nil, err } - val, err := json.MarshalIndent(batchInfo, "", " ") + val, err := json.MarshalIndent(components.batch, "", " ") if err != nil { return nil, err } - cryptoHandler, err := ethereumClient.NewCryptoHandler(cfg.Eth.PrivateKeyFile) + components.cryptoHandler, err = ethereumClient.NewCryptoHandler(cfg.Eth.PrivateKeyFile) if err != nil { return nil, err } - log.Info("signing batch", "message hash", batchInfo.MessageHash.String(), - "public key", cryptoHandler.GetAddress().String()) + log.Info("signing batch", "message hash", components.batch.MessageHash.String(), + "public key", components.cryptoHandler.GetAddress().String()) - signature, err := cryptoHandler.Sign(batchInfo.MessageHash) + signature, err := components.cryptoHandler.Sign(components.batch.MessageHash) if err != nil { return nil, err } @@ -226,8 +264,8 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) (*interna } sigInfo := ðereum.SignatureInfo{ - Address: cryptoHandler.GetAddress().String(), - MessageHash: batchInfo.MessageHash.String(), + Address: components.cryptoHandler.GetAddress().String(), + MessageHash: components.batch.MessageHash.String(), Signature: hex.EncodeToString(signature), } @@ -246,12 +284,7 @@ func generateAndSign(ctx *cli.Context, cfg config.MigrationToolConfig) (*interna return nil, err } - return &internalComponents{ - batch: batchInfo, - cryptoHandler: cryptoHandler, - ethClient: ethClient, - ethereumChainWrapper: ethereumChainWrapper, - }, nil + return components, nil } func executeTransfer(ctx *cli.Context, cfg config.MigrationToolConfig) error { diff --git a/executors/ethereum/common.go b/executors/ethereum/common.go index dcbb6923..c4a93cdb 100644 --- a/executors/ethereum/common.go +++ b/executors/ethereum/common.go @@ -1,19 +1,24 @@ package ethereum import ( + "fmt" "math/big" + "strings" "github.com/ethereum/go-ethereum/common" ) // DepositInfo is the deposit info list type DepositInfo struct { - DepositNonce uint64 `json:"DepositNonce"` - Token string `json:"Token"` - ContractAddressString string `json:"ContractAddress"` - ContractAddress common.Address `json:"-"` - Amount *big.Int `json:"-"` - AmountString string `json:"Amount"` + DepositNonce uint64 `json:"DepositNonce"` + Token string `json:"Token"` + ContractAddressString string `json:"ContractAddress"` + Decimals byte `json:"Decimals"` + ContractAddress common.Address `json:"-"` + Amount *big.Int `json:"-"` + AmountString string `json:"Amount"` + DenominatedAmount *big.Float `json:"-"` + DenominatedAmountString string `json:"DenominatedAmount"` } // BatchInfo is the batch info list @@ -31,3 +36,65 @@ type SignatureInfo struct { MessageHash string `json:"MessageHash"` Signature string `json:"Signature"` } + +// TokensBalancesDisplayString will convert the deposit balances into a human-readable string +func TokensBalancesDisplayString(batchInfo *BatchInfo) string { + maxTokenLen := 0 + maxIntegerValueLen := 0 + integerIndex := 0 + tokenIntegerSpace := make(map[string]int) + decimalSeparator := "." // src/math/big/ftoa.go L302 + for _, deposit := range batchInfo.DepositsInfo { + if len(deposit.Token) > maxTokenLen { + maxTokenLen = len(deposit.Token) + } + + valueParts := strings.Split(deposit.DenominatedAmountString, decimalSeparator) + integerPart := valueParts[integerIndex] + if len(integerPart) > maxIntegerValueLen { + maxIntegerValueLen = len(valueParts[integerIndex]) + } + tokenIntegerSpace[deposit.Token] = len(valueParts[integerIndex]) + } + + tokens := make([]string, 0, len(batchInfo.DepositsInfo)) + for _, deposit := range batchInfo.DepositsInfo { + spaceRequired := strings.Repeat(" ", maxTokenLen-len(deposit.Token)+maxIntegerValueLen-tokenIntegerSpace[deposit.Token]) + tokenInfo := fmt.Sprintf(" %s: %s%s", deposit.Token, spaceRequired, deposit.DenominatedAmountString) + + tokens = append(tokens, tokenInfo) + } + + return strings.Join(tokens, "\n") +} + +// ConvertPartialMigrationStringToMap converts the partial migration string to its map representation +func ConvertPartialMigrationStringToMap(partialMigration string) (map[string]*big.Float, error) { + partsSeparator := "," + tokenAmountSeparator := ":" + parts := strings.Split(partialMigration, partsSeparator) + + partialMap := make(map[string]*big.Float) + for _, part := range parts { + part = strings.Trim(part, " \t\n") + splt := strings.Split(part, tokenAmountSeparator) + if len(splt) != 2 { + return nil, fmt.Errorf("%w at token %s, invalid format", errInvalidPartialMigrationString, part) + } + + amount, ok := big.NewFloat(0).SetString(splt[1]) + if !ok { + return nil, fmt.Errorf("%w at token %s, not a number", errInvalidPartialMigrationString, part) + } + + token := splt[0] + if partialMap[token] == nil { + partialMap[token] = big.NewFloat(0).Set(amount) + continue + } + + partialMap[token].Add(partialMap[token], amount) + } + + return partialMap, nil +} diff --git a/executors/ethereum/common_test.go b/executors/ethereum/common_test.go new file mode 100644 index 00000000..12b75a1f --- /dev/null +++ b/executors/ethereum/common_test.go @@ -0,0 +1,133 @@ +package ethereum + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTokensBalancesDisplayString(t *testing.T) { + t.Parallel() + + batchInfo := &BatchInfo{ + DepositsInfo: []*DepositInfo{ + { + Token: "ETHUSDC-220753", + DenominatedAmountString: "5900401.957669", + }, + { + Token: "ETHUTK-8cdf7a", + DenominatedAmountString: "224564287.8192652", + }, + { + Token: "ETHUSDT-9c73c6", + DenominatedAmountString: "542188.933704", + }, + { + Token: "ETHBUSD-450923", + DenominatedAmountString: "22294.352736330155", + }, + { + Token: "ETHHMT-18538a", + DenominatedAmountString: "435", + }, + { + Token: "ETHCGG-ee4e0c", + DenominatedAmountString: "1594290.750967581", + }, + { + Token: "ETHINFRA-60a3bf2", + DenominatedAmountString: "141172.59598039952", + }, + { + Token: "ETHWBTC-74e282", + DenominatedAmountString: "39.46386326", + }, + { + Token: "ETHWETH-e1c126", + DenominatedAmountString: "664.1972941951753", + }, + { + Token: "ETHWSDAI-572803", + DenominatedAmountString: "5431.516086574386", + }, + { + Token: "ETHWDAI-bd65f9", + DenominatedAmountString: "118591.44846500318", + }, + { + Token: "ETHUMB-291202", + DenominatedAmountString: "4065258.3239772925", + }, + }, + } + + expectedString := + ` ETHUSDC-220753: 5900401.957669 + ETHUTK-8cdf7a: 224564287.8192652 + ETHUSDT-9c73c6: 542188.933704 + ETHBUSD-450923: 22294.352736330155 + ETHHMT-18538a: 435 + ETHCGG-ee4e0c: 1594290.750967581 + ETHINFRA-60a3bf2: 141172.59598039952 + ETHWBTC-74e282: 39.46386326 + ETHWETH-e1c126: 664.1972941951753 + ETHWSDAI-572803: 5431.516086574386 + ETHWDAI-bd65f9: 118591.44846500318 + ETHUMB-291202: 4065258.3239772925` + + assert.Equal(t, expectedString, TokensBalancesDisplayString(batchInfo)) +} + +func TestConvertPartialMigrationStringToMap(t *testing.T) { + t.Parallel() + + t.Run("invalid part should error", func(t *testing.T) { + t.Parallel() + + str := "k,f" + results, err := ConvertPartialMigrationStringToMap(str) + assert.Nil(t, results) + assert.ErrorIs(t, err, errInvalidPartialMigrationString) + assert.Contains(t, err.Error(), "at token k, invalid format") + + str = "k:1:2,f" + results, err = ConvertPartialMigrationStringToMap(str) + assert.Nil(t, results) + assert.ErrorIs(t, err, errInvalidPartialMigrationString) + assert.Contains(t, err.Error(), "at token k:1:2, invalid format") + }) + t.Run("amount is empty should error", func(t *testing.T) { + t.Parallel() + + str := "k:,f:1" + results, err := ConvertPartialMigrationStringToMap(str) + assert.Nil(t, results) + assert.ErrorIs(t, err, errInvalidPartialMigrationString) + assert.Contains(t, err.Error(), "at token k:, not a number") + + str = "k:1d2,f" + results, err = ConvertPartialMigrationStringToMap(str) + assert.Nil(t, results) + assert.ErrorIs(t, err, errInvalidPartialMigrationString) + assert.Contains(t, err.Error(), "at token k:1d2, not a number") + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + str := "k:1,f:1,k:2.2,g:0.001,h:0" + results, err := ConvertPartialMigrationStringToMap(str) + assert.Nil(t, err) + + expectedResults := map[string]*big.Float{ + "k": big.NewFloat(3.2), + "f": big.NewFloat(1), + "g": big.NewFloat(0.001), + "h": big.NewFloat(0), + } + + assert.Nil(t, err) + assert.Equal(t, expectedResults, results) + }) +} diff --git a/executors/ethereum/errors.go b/executors/ethereum/errors.go index e9ab9782..d1855ce4 100644 --- a/executors/ethereum/errors.go +++ b/executors/ethereum/errors.go @@ -3,15 +3,16 @@ package ethereum import "errors" var ( - errEmptyTokensList = errors.New("empty tokens list") - errNilMvxDataGetter = errors.New("nil MultiversX data getter") - errNilErc20ContractsHolder = errors.New("nil ERC20 contracts holder") - errWrongERC20AddressResponse = errors.New("wrong ERC20 address response") - errNilLogger = errors.New("nil logger") - errNilCryptoHandler = errors.New("nil crypto handler") - errNilEthereumChainWrapper = errors.New("nil Ethereum chain wrapper") - errQuorumNotReached = errors.New("quorum not reached") - errInvalidSignature = errors.New("invalid signature") - errMultisigContractPaused = errors.New("multisig contract paused") - errNilGasHandler = errors.New("nil gas handler") + errEmptyTokensList = errors.New("empty tokens list") + errNilMvxDataGetter = errors.New("nil MultiversX data getter") + errNilErc20ContractsHolder = errors.New("nil ERC20 contracts holder") + errWrongERC20AddressResponse = errors.New("wrong ERC20 address response") + errNilLogger = errors.New("nil logger") + errNilCryptoHandler = errors.New("nil crypto handler") + errNilEthereumChainWrapper = errors.New("nil Ethereum chain wrapper") + errQuorumNotReached = errors.New("quorum not reached") + errInvalidSignature = errors.New("invalid signature") + errMultisigContractPaused = errors.New("multisig contract paused") + errNilGasHandler = errors.New("nil gas handler") + errInvalidPartialMigrationString = errors.New("invalid partial migration string") ) diff --git a/executors/ethereum/interface.go b/executors/ethereum/interface.go index a12eec9e..14f9e537 100644 --- a/executors/ethereum/interface.go +++ b/executors/ethereum/interface.go @@ -18,6 +18,7 @@ type TokensMapper interface { // Erc20ContractsHolder defines the Ethereum ERC20 contract operations type Erc20ContractsHolder interface { BalanceOf(ctx context.Context, erc20Address common.Address, address common.Address) (*big.Int, error) + Decimals(ctx context.Context, address common.Address) (uint8, error) IsInterfaceNil() bool } diff --git a/executors/ethereum/migrationBatchCreator.go b/executors/ethereum/migrationBatchCreator.go index a1e27101..9e88271f 100644 --- a/executors/ethereum/migrationBatchCreator.go +++ b/executors/ethereum/migrationBatchCreator.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/multiversx/mx-bridge-eth-go/clients/ethereum" "github.com/multiversx/mx-bridge-eth-go/core/batchProcessor" - "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" logger "github.com/multiversx/mx-chain-logger-go" ) @@ -61,7 +60,7 @@ func NewMigrationBatchCreator(args ArgsMigrationBatchCreator) (*migrationBatchCr } // CreateBatchInfo creates an instance of type BatchInfo -func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, trimAmount core.OptionalUint64) (*BatchInfo, error) { +func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, partialMigration map[string]*big.Float) (*BatchInfo, error) { creator.logger.Info("started the batch creation process...") depositStart := uint64(0) // deposits inside a batch are not tracked, we can start from 0 @@ -78,7 +77,11 @@ func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSa creator.logger.Info("fetched Ethereum contracts state", "free batch ID", freeBatchID, "time took", endTime.Sub(startTime)) - tokensList, err := creator.getTokensList(ctx) + if partialMigration == nil { + partialMigration = make(map[string]*big.Float) + } + + tokensList, err := creator.getTokensList(ctx, partialMigration) if err != nil { return nil, err } @@ -92,7 +95,7 @@ func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSa creator.logger.Info("fetched ERC20 contract addresses") - err = creator.fetchBalances(ctx, deposits, trimAmount) + err = creator.fetchBalances(ctx, deposits, partialMigration) if err != nil { return nil, err } @@ -172,7 +175,7 @@ func (creator *migrationBatchCreator) checkAvailableBatch( return nil } -func (creator *migrationBatchCreator) getTokensList(ctx context.Context) ([]string, error) { +func (creator *migrationBatchCreator) getTokensList(ctx context.Context, partialMigration map[string]*big.Float) ([]string, error) { tokens, err := creator.mvxDataGetter.GetAllKnownTokens(ctx) if err != nil { return nil, err @@ -183,6 +186,12 @@ func (creator *migrationBatchCreator) getTokensList(ctx context.Context) ([]stri stringTokens := make([]string, 0, len(tokens)) for _, token := range tokens { + if len(partialMigration) > 1 && partialMigration[string(token)] == nil { + // partial migration was set, but for the current token in this deposit a value was not given + // skip this deposit + continue + } + stringTokens = append(stringTokens, string(token)) } @@ -215,15 +224,28 @@ func (creator *migrationBatchCreator) fetchERC20ContractsAddresses(ctx context.C return deposits, nil } -func (creator *migrationBatchCreator) fetchBalances(ctx context.Context, deposits []*DepositInfo, trimAmount core.OptionalUint64) error { +func (creator *migrationBatchCreator) fetchBalances(ctx context.Context, deposits []*DepositInfo, partialMigration map[string]*big.Float) error { for _, deposit := range deposits { balance, err := creator.erc20ContractsHolder.BalanceOf(ctx, deposit.ContractAddress, creator.safeContractAddress) if err != nil { return fmt.Errorf("%w for address %s in ERC20 contract %s", err, creator.safeContractAddress.String(), deposit.ContractAddress.String()) } - if trimAmount.HasValue { - newBalance := big.NewInt(0).SetUint64(trimAmount.Value) + decimals, err := creator.erc20ContractsHolder.Decimals(ctx, deposit.ContractAddress) + if err != nil { + return fmt.Errorf("%w for in ERC20 contract %s", err, deposit.ContractAddress.String()) + } + deposit.Decimals = decimals + + trimAmount := partialMigration[deposit.Token] + if trimAmount != nil { + denominatedTrimAmount := big.NewFloat(0).Set(trimAmount) + multiplier := big.NewInt(10) + multiplier.Exp(multiplier, big.NewInt(int64(deposit.Decimals)), nil) + denominatedTrimAmount.Mul(denominatedTrimAmount, big.NewFloat(0).SetInt(multiplier)) + + newBalance := big.NewInt(0) + denominatedTrimAmount.Int(newBalance) if balance.Cmp(newBalance) > 0 { creator.logger.Warn("applied denominated value", "balance", balance.String(), "new value to consider", newBalance.String()) balance = newBalance @@ -234,6 +256,13 @@ func (creator *migrationBatchCreator) fetchBalances(ctx context.Context, deposit deposit.Amount = balance deposit.AmountString = balance.String() + + divider := big.NewInt(10) + divider.Exp(divider, big.NewInt(int64(decimals)), nil) + + deposit.DenominatedAmount = big.NewFloat(0).SetInt(balance) + deposit.DenominatedAmount.Quo(deposit.DenominatedAmount, big.NewFloat(0).SetInt(divider)) + deposit.DenominatedAmountString = deposit.DenominatedAmount.Text('f', -1) } return nil diff --git a/executors/ethereum/migrationBatchCreator_test.go b/executors/ethereum/migrationBatchCreator_test.go index 207fd9e0..277aa319 100644 --- a/executors/ethereum/migrationBatchCreator_test.go +++ b/executors/ethereum/migrationBatchCreator_test.go @@ -12,16 +12,19 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/multiversx/mx-bridge-eth-go/testsCommon/bridge" - "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-go/testscommon" "github.com/stretchr/testify/assert" ) var safeContractAddress = common.HexToAddress(strings.Repeat("9", 40)) -var tkn1Erc20Address = bytes.Repeat([]byte("2"), 20) -var tkn2Erc20Address = bytes.Repeat([]byte("3"), 20) +var tkn1Erc20Address = bytes.Repeat([]byte("1"), 20) +var tkn2Erc20Address = bytes.Repeat([]byte("2"), 20) +var tkn3Erc20Address = bytes.Repeat([]byte("3"), 20) +var tkn4Erc20Address = bytes.Repeat([]byte("4"), 20) var balanceOfTkn1 = big.NewInt(19) var balanceOfTkn2 = big.NewInt(38) +var balanceOfTkn3 = big.NewInt(138) +var balanceOfTkn4 = big.NewInt(1137) var expectedErr = errors.New("expected error") func createMockArgsForMigrationBatchCreator() ArgsMigrationBatchCreator { @@ -31,6 +34,8 @@ func createMockArgsForMigrationBatchCreator() ArgsMigrationBatchCreator { return [][]byte{ []byte("tkn1"), []byte("tkn2"), + []byte("tkn3"), + []byte("tkn4"), }, nil }, GetERC20AddressForTokenIdCalled: func(ctx context.Context, tokenId []byte) ([][]byte, error) { @@ -292,7 +297,7 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.Equal(t, expectedErr, err) assert.Nil(t, batch) }) @@ -312,7 +317,7 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.Equal(t, expectedErr, err) assert.Nil(t, batch) }) @@ -332,7 +337,7 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.ErrorIs(t, err, errEmptyTokensList) assert.Nil(t, batch) }) @@ -350,7 +355,7 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.Equal(t, expectedErr, err) assert.Nil(t, batch) }) @@ -368,7 +373,7 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.ErrorIs(t, err, errWrongERC20AddressResponse) assert.Nil(t, batch) }) @@ -388,7 +393,7 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.ErrorIs(t, err, expectedErr) assert.Nil(t, batch) }) @@ -403,6 +408,12 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { if string(sourceBytes) == "tkn2" { return [][]byte{tkn2Erc20Address}, nil } + if string(sourceBytes) == "tkn3" { + return [][]byte{tkn3Erc20Address}, nil + } + if string(sourceBytes) == "tkn4" { + return [][]byte{tkn4Erc20Address}, nil + } return nil, fmt.Errorf("unexpected source bytes") } @@ -416,9 +427,31 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { if string(erc20Address.Bytes()) == string(tkn2Erc20Address) { return balanceOfTkn2, nil } + if string(erc20Address.Bytes()) == string(tkn3Erc20Address) { + return balanceOfTkn3, nil + } + if string(erc20Address.Bytes()) == string(tkn4Erc20Address) { + return balanceOfTkn4, nil + } return nil, fmt.Errorf("unexpected ERC20 contract address") }, + DecimalsCalled: func(ctx context.Context, erc20Address common.Address) (uint8, error) { + if string(erc20Address.Bytes()) == string(tkn1Erc20Address) { + return 3, nil + } + if string(erc20Address.Bytes()) == string(tkn2Erc20Address) { + return 18, nil + } + if string(erc20Address.Bytes()) == string(tkn2Erc20Address) { + return 0, nil + } + if string(erc20Address.Bytes()) == string(tkn4Erc20Address) { + return 1, nil + } + + return 0, nil + }, } args.EthereumChainWrapper = &bridge.EthereumClientWrapperStub{ WasBatchExecutedCalled: func(ctx context.Context, batchNonce *big.Int) (bool, error) { @@ -427,33 +460,61 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { } creator, _ := NewMigrationBatchCreator(args) - t.Run("without trim", func(t *testing.T) { + t.Run("without migration map", func(t *testing.T) { expectedBatch := &BatchInfo{ OldSafeContractAddress: safeContractAddress.String(), NewSafeContractAddress: newSafeContractAddress.String(), BatchID: firstFreeBatchId, - MessageHash: common.HexToHash("0x5650b9dcc3283c624328422a6a41dd3305f86c5456762f63110a2fc4e23f5162"), + MessageHash: common.HexToHash("0xa0d36274c96845ee51e76980df39c44cdabfa41b85238457cab8834ad8410447"), DepositsInfo: []*DepositInfo{ { - DepositNonce: 1, - Token: "tkn1", - ContractAddressString: common.BytesToAddress(tkn1Erc20Address).String(), - ContractAddress: common.BytesToAddress(tkn1Erc20Address), - Amount: big.NewInt(19), - AmountString: "19", + DepositNonce: 1, + Token: "tkn1", + ContractAddressString: common.BytesToAddress(tkn1Erc20Address).String(), + Decimals: 3, + ContractAddress: common.BytesToAddress(tkn1Erc20Address), + Amount: big.NewInt(19), + AmountString: "19", + DenominatedAmountString: "0.019", + }, + { + DepositNonce: 2, + Token: "tkn2", + ContractAddressString: common.BytesToAddress(tkn2Erc20Address).String(), + Decimals: 18, + ContractAddress: common.BytesToAddress(tkn2Erc20Address), + Amount: big.NewInt(38), + AmountString: "38", + DenominatedAmountString: "0.000000000000000038", }, { - DepositNonce: 2, - Token: "tkn2", - ContractAddressString: common.BytesToAddress(tkn2Erc20Address).String(), - ContractAddress: common.BytesToAddress(tkn2Erc20Address), - Amount: big.NewInt(38), - AmountString: "38", + DepositNonce: 3, + Token: "tkn3", + ContractAddressString: common.BytesToAddress(tkn3Erc20Address).String(), + Decimals: 0, + ContractAddress: common.BytesToAddress(tkn3Erc20Address), + Amount: big.NewInt(138), + AmountString: "138", + DenominatedAmountString: "138", + }, + { + DepositNonce: 4, + Token: "tkn4", + ContractAddressString: common.BytesToAddress(tkn4Erc20Address).String(), + Decimals: 1, + ContractAddress: common.BytesToAddress(tkn4Erc20Address), + Amount: big.NewInt(1137), + AmountString: "1137", + DenominatedAmountString: "113.7", }, }, } + expectedBatch.DepositsInfo[0].DenominatedAmount, _ = big.NewFloat(0).SetString("0.019") + expectedBatch.DepositsInfo[1].DenominatedAmount, _ = big.NewFloat(0).SetString("0.000000000000000038") + expectedBatch.DepositsInfo[2].DenominatedAmount, _ = big.NewFloat(0).SetString("138") + expectedBatch.DepositsInfo[3].DenominatedAmount, _ = big.NewFloat(0).SetString("113.7") - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, core.OptionalUint64{}) + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, nil) assert.Nil(t, err) assert.Equal(t, expectedBatch, batch) }) @@ -462,32 +523,51 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { OldSafeContractAddress: safeContractAddress.String(), NewSafeContractAddress: newSafeContractAddress.String(), BatchID: firstFreeBatchId, - MessageHash: common.HexToHash("0x7dc48af8b0431d100adefaed79bacd0c33ab0fdcc11723de6eaa3f158595a097"), + MessageHash: common.HexToHash("0xb726ee06a2fd99ef8e78cf97dc25522260796df572cd3967a6e750c3a1201276"), DepositsInfo: []*DepositInfo{ { - DepositNonce: 1, - Token: "tkn1", - ContractAddressString: common.BytesToAddress(tkn1Erc20Address).String(), - ContractAddress: common.BytesToAddress(tkn1Erc20Address), - Amount: big.NewInt(19), - AmountString: "19", + DepositNonce: 1, + Token: "tkn1", + ContractAddressString: common.BytesToAddress(tkn1Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn1Erc20Address), + Amount: big.NewInt(17), + AmountString: "17", + DenominatedAmountString: "0.017", + Decimals: 3, }, { - DepositNonce: 2, - Token: "tkn2", - ContractAddressString: common.BytesToAddress(tkn2Erc20Address).String(), - ContractAddress: common.BytesToAddress(tkn2Erc20Address), - Amount: big.NewInt(20), - AmountString: "20", + DepositNonce: 2, + Token: "tkn2", + ContractAddressString: common.BytesToAddress(tkn2Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn2Erc20Address), + Amount: big.NewInt(20), + AmountString: "20", + DenominatedAmountString: "0.00000000000000002", + Decimals: 18, + }, + { + DepositNonce: 3, + Token: "tkn3", + ContractAddressString: common.BytesToAddress(tkn3Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn3Erc20Address), + Amount: big.NewInt(120), + AmountString: "120", + DenominatedAmountString: "120", + Decimals: 0, }, }, } + expectedBatch.DepositsInfo[0].DenominatedAmount, _ = big.NewFloat(0).SetString("0.017") + expectedBatch.DepositsInfo[1].DenominatedAmount, _ = big.NewFloat(0).SetString("0.000000000000000020") + expectedBatch.DepositsInfo[2].DenominatedAmount, _ = big.NewFloat(0).SetString("120") - trimValue := core.OptionalUint64{ - Value: 20, - HasValue: true, + partialMap := map[string]*big.Float{ + "tkn1": big.NewFloat(0.017), + "tkn3": big.NewFloat(120), } - batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, trimValue) + partialMap["tkn2"], _ = big.NewFloat(0).SetString("0.000000000000000020") + + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, partialMap) assert.Nil(t, err) assert.Equal(t, expectedBatch, batch) }) diff --git a/integrationTests/mock/mockWriter.go b/integrationTests/mock/mockWriter.go index 50ac35ee..5db37ca7 100644 --- a/integrationTests/mock/mockWriter.go +++ b/integrationTests/mock/mockWriter.go @@ -4,26 +4,44 @@ import "strings" type mockLogObserver struct { expectedStringInLog string + exceptStrings []string logFoundChan chan struct{} } // NewMockLogObserver returns a new instance of mockLogObserver -func NewMockLogObserver(expectedStringInLog string) *mockLogObserver { +func NewMockLogObserver(expectedStringInLog string, exceptStrings ...string) *mockLogObserver { return &mockLogObserver{ expectedStringInLog: expectedStringInLog, + exceptStrings: exceptStrings, logFoundChan: make(chan struct{}, 1), } } // Write is called by the logger func (observer *mockLogObserver) Write(log []byte) (n int, err error) { - if strings.Contains(string(log), observer.expectedStringInLog) { - observer.logFoundChan <- struct{}{} + str := string(log) + if !strings.Contains(str, observer.expectedStringInLog) { + return 0, nil } + if observer.stringIsExcepted(str) { + return 0, nil + } + + observer.logFoundChan <- struct{}{} return 0, nil } +func (observer *mockLogObserver) stringIsExcepted(str string) bool { + for _, exceptString := range observer.exceptStrings { + if strings.Contains(str, exceptString) { + return true + } + } + + return false +} + // LogFoundChan returns the internal chan func (observer *mockLogObserver) LogFoundChan() chan struct{} { return observer.logFoundChan diff --git a/integrationTests/mock/multiversXContractStateMock.go b/integrationTests/mock/multiversXContractStateMock.go index b9502856..8326b5dd 100644 --- a/integrationTests/mock/multiversXContractStateMock.go +++ b/integrationTests/mock/multiversXContractStateMock.go @@ -270,6 +270,8 @@ func (mock *multiversXContractStateMock) processVmRequests(vmRequest *data.VmVal return mock.vmRequestGetMintBalances(vmRequest), nil case "getBurnBalances": return mock.vmRequestGetBurnBalances(vmRequest), nil + case "getLastBatchId": + return mock.vmRequestGetLastBatchId(vmRequest), nil } panic("unimplemented function: " + vmRequest.FuncName) @@ -330,7 +332,7 @@ func (mock *multiversXContractStateMock) vmRequestGetStatusesAfterExecution(_ *d } func (mock *multiversXContractStateMock) sign(dataSplit []string, tx *transaction.FrontendTransaction) { - actionID := getActionIDFromString(dataSplit[1]) + actionID := getBigIntFromString(dataSplit[1]) if !mock.actionIDExists(actionID) { panic(fmt.Sprintf("attempted to sign on a missing action ID: %v as big int, raw: %s", actionID, dataSplit[1])) } @@ -344,7 +346,7 @@ func (mock *multiversXContractStateMock) sign(dataSplit []string, tx *transactio } func (mock *multiversXContractStateMock) performAction(dataSplit []string, _ *transaction.FrontendTransaction) { - actionID := getActionIDFromString(dataSplit[1]) + actionID := getBigIntFromString(dataSplit[1]) if !mock.actionIDExists(actionID) { panic(fmt.Sprintf("attempted to perform on a missing action ID: %v as big int, raw: %s", actionID, dataSplit[1])) } @@ -360,7 +362,7 @@ func (mock *multiversXContractStateMock) performAction(dataSplit []string, _ *tr } func (mock *multiversXContractStateMock) vmRequestWasActionExecuted(vmRequest *data.VmValueRequest) *data.VmValuesResponseData { - actionID := getActionIDFromString(vmRequest.Args[0]) + actionID := getBigIntFromString(vmRequest.Args[0]) if mock.performedAction == nil { return createOkVmResponse([][]byte{BoolToByteSlice(false)}) @@ -390,7 +392,7 @@ func (mock *multiversXContractStateMock) actionIDExists(actionID *big.Int) bool } func (mock *multiversXContractStateMock) vmRequestQuorumReached(vmRequest *data.VmValueRequest) *data.VmValuesResponseData { - actionID := getActionIDFromString(vmRequest.Args[0]) + actionID := getBigIntFromString(vmRequest.Args[0]) m, found := mock.signedActionIDs[actionID.String()] if !found { return createOkVmResponse([][]byte{BoolToByteSlice(false)}) @@ -434,6 +436,10 @@ func (mock *multiversXContractStateMock) vmRequestGetCurrentPendingBatch(_ *data return createOkVmResponse(make([][]byte, 0)) } + return mock.responseWithPendingBatch() +} + +func (mock *multiversXContractStateMock) responseWithPendingBatch() *data.VmValuesResponseData { args := [][]byte{mock.pendingBatch.Nonce.Bytes()} // first non-empty slice for _, deposit := range mock.pendingBatch.MultiversXDeposits { args = append(args, make([]byte, 0)) // mocked block nonce @@ -446,7 +452,16 @@ func (mock *multiversXContractStateMock) vmRequestGetCurrentPendingBatch(_ *data return createOkVmResponse(args) } -func (mock *multiversXContractStateMock) vmRequestGetBatch(_ *data.VmValueRequest) *data.VmValuesResponseData { +func (mock *multiversXContractStateMock) vmRequestGetBatch(request *data.VmValueRequest) *data.VmValuesResponseData { + if mock.pendingBatch == nil { + return createOkVmResponse(make([][]byte, 0)) + } + + nonce := getBigIntFromString(request.Args[0]) + if nonce.Cmp(mock.pendingBatch.Nonce) == 0 { + return mock.responseWithPendingBatch() + } + return createOkVmResponse(make([][]byte, 0)) } @@ -456,7 +471,7 @@ func (mock *multiversXContractStateMock) setPendingBatch(pendingBatch *Multivers func (mock *multiversXContractStateMock) vmRequestSigned(request *data.VmValueRequest) *data.VmValuesResponseData { hexAddress := request.Args[0] - actionID := getActionIDFromString(request.Args[1]) + actionID := getBigIntFromString(request.Args[1]) actionIDMap, found := mock.signedActionIDs[actionID.String()] if !found { @@ -512,7 +527,14 @@ func (mock *multiversXContractStateMock) vmRequestGetBurnBalances(vmRequest *dat return createOkVmResponse([][]byte{mock.getBurnBalances(address).Bytes()}) } -func getActionIDFromString(data string) *big.Int { +func (mock *multiversXContractStateMock) vmRequestGetLastBatchId(_ *data.VmValueRequest) *data.VmValuesResponseData { + if mock.pendingBatch == nil { + return createOkVmResponse([][]byte{big.NewInt(0).Bytes()}) + } + return createOkVmResponse([][]byte{mock.pendingBatch.Nonce.Bytes()}) +} + +func getBigIntFromString(data string) *big.Int { buff, err := hex.DecodeString(data) if err != nil { panic(err) diff --git a/integrationTests/relayers/slowTests/edgeCases_test.go b/integrationTests/relayers/slowTests/edgeCases_test.go new file mode 100644 index 00000000..98470e15 --- /dev/null +++ b/integrationTests/relayers/slowTests/edgeCases_test.go @@ -0,0 +1,106 @@ +//go:build slow + +package slowTests + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/multiversx/mx-bridge-eth-go/integrationTests/mock" + "github.com/multiversx/mx-bridge-eth-go/integrationTests/relayers/slowTests/framework" + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/stretchr/testify/require" +) + +func TestRelayerShouldExecuteSimultaneousSwapsAndNotCatchErrors(t *testing.T) { + t.Skip("TODO: fix this test") + + errorString := "ERROR" + mockLogObserver := mock.NewMockLogObserver(errorString, "got invalid action ID") + err := logger.AddLogObserver(mockLogObserver, &logger.PlainFormatter{}) + require.NoError(t, err) + defer func() { + require.NoError(t, logger.RemoveLogObserver(mockLogObserver)) + }() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + stopChan := make(chan error, 1000) // ensure sufficient error buffer + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-mockLogObserver.LogFoundChan(): + stopChan <- errors.New("logger should have not caught errors") + } + } + }() + + usdcToken := GenerateTestUSDCToken() + usdcToken.TestOperations = []framework.TokenOperations{ + { + ValueToTransferToMvx: big.NewInt(5000), + ValueToSendFromMvX: big.NewInt(200), + MvxSCCallData: nil, + MvxFaultySCCall: false, + MvxForceSCCall: false, + }, + } + usdcToken.ESDTSafeExtraBalance = big.NewInt(50) + usdcToken.ExtraBalances = map[string]framework.ExtraBalanceHolder{ + "Alice": { + SentAmount: big.NewInt(-5000), + ReceivedAmount: big.NewInt(0), + }, + "Bob": { + SentAmount: big.NewInt(-200), + ReceivedAmount: big.NewInt(5000), + }, + } + + _ = testRelayersWithChainSimulatorAndTokensForSimultaneousSwaps( + t, + stopChan, + usdcToken, + ) +} + +func testRelayersWithChainSimulatorAndTokensForSimultaneousSwaps(tb testing.TB, manualStopChan chan error, tokens ...framework.TestTokenParams) *framework.TestSetup { + startsFromEthFlow := &startsFromEthereumEdgecaseFlow{ + TB: tb, + tokens: tokens, + } + + setupFunc := func(tb testing.TB, setup *framework.TestSetup) { + startsFromEthFlow.setup = setup + + setup.IssueAndConfigureTokens(tokens...) + setup.MultiversxHandler.CheckForZeroBalanceOnReceivers(setup.Ctx, tokens...) + setup.CreateBatchOnEthereum(setup.MultiversxHandler.CalleeScAddress, startsFromEthFlow.tokens...) + } + + processFunc := func(tb testing.TB, setup *framework.TestSetup) bool { + if startsFromEthFlow.process() { + setup.TestWithdrawTotalFeesOnEthereumForTokens(startsFromEthFlow.tokens...) + + return true + } + + setup.EthereumHandler.SimulatedChain.Commit() + setup.ChainSimulator.GenerateBlocks(setup.Ctx, 1) + require.LessOrEqual(tb, setup.ScCallerModuleInstance.GetNumSentTransaction(), setup.GetNumScCallsOperations()) + + return false + } + + return testRelayersWithChainSimulator(tb, + setupFunc, + processFunc, + manualStopChan, + ) +} diff --git a/integrationTests/relayers/slowTests/framework/multiversxHandler.go b/integrationTests/relayers/slowTests/framework/multiversxHandler.go index 580bec33..64bd80db 100644 --- a/integrationTests/relayers/slowTests/framework/multiversxHandler.go +++ b/integrationTests/relayers/slowTests/framework/multiversxHandler.go @@ -46,7 +46,6 @@ const ( setWrappingContractAddressFunction = "setWrappingContractAddress" changeOwnerAddressFunction = "ChangeOwnerAddress" setEsdtSafeOnMultiTransferFunction = "setEsdtSafeOnMultiTransfer" - setEsdtSafeOnWrapperFunction = "setEsdtSafeContractAddress" setEsdtSafeAddressFunction = "setEsdtSafeAddress" stakeFunction = "stake" unpauseFunction = "unpause" @@ -67,6 +66,7 @@ const ( multiTransferEsdtSetMaxBridgedAmountForTokenFunction = "multiTransferEsdtSetMaxBridgedAmountForToken" submitBatchFunction = "submitBatch" unwrapTokenCreateTransactionFunction = "unwrapTokenCreateTransaction" + createTransactionFunction = "createTransaction" setBridgedTokensWrapperAddressFunction = "setBridgedTokensWrapperAddress" setMultiTransferAddressFunction = "setMultiTransferAddress" withdrawRefundFeesForEthereumFunction = "withdrawRefundFeesForEthereum" @@ -135,7 +135,6 @@ func (handler *MultiversxHandler) DeployAndSetContracts(ctx context.Context) { handler.wireMultiTransfer(ctx) handler.wireSCProxy(ctx) - handler.wireWrapper(ctx) handler.wireSafe(ctx) handler.changeOwners(ctx) @@ -327,22 +326,6 @@ func (handler *MultiversxHandler) wireSCProxy(ctx context.Context) { log.Info("Set in SC proxy contract the safe contract", "transaction hash", hash, "status", txResult.Status) } -func (handler *MultiversxHandler) wireWrapper(ctx context.Context) { - // setEsdtSafeOnWrapper - hash, txResult := handler.ChainSimulator.ScCall( - ctx, - handler.OwnerKeys.MvxSk, - handler.WrapperAddress, - zeroStringValue, - setCallsGasLimit, - setEsdtSafeOnWrapperFunction, - []string{ - handler.SafeAddress.Hex(), - }, - ) - log.Info("Set in wrapper contract the safe contract", "transaction hash", hash, "status", txResult.Status) -} - func (handler *MultiversxHandler) wireSafe(ctx context.Context) { // setBridgedTokensWrapperAddress hash, txResult := handler.ChainSimulator.ScCall( @@ -955,13 +938,51 @@ func (handler *MultiversxHandler) submitAggregatorBatchForKey(ctx context.Contex } // SendDepositTransactionFromMultiversx will send the deposit transaction from MultiversX -func (handler *MultiversxHandler) SendDepositTransactionFromMultiversx(ctx context.Context, from KeysHolder, to KeysHolder, token *TokenData, value *big.Int) { +func (handler *MultiversxHandler) SendDepositTransactionFromMultiversx(ctx context.Context, from KeysHolder, to KeysHolder, token *TokenData, params TestTokenParams, value *big.Int) { + if params.HasChainSpecificToken { + handler.unwrapCreateTransaction(ctx, token, from, to, value) + return + } + + handler.createTransactionWithoutUnwrap(ctx, token, from, to, value) +} + +func (handler *MultiversxHandler) createTransactionWithoutUnwrap( + ctx context.Context, + token *TokenData, + from KeysHolder, + to KeysHolder, + value *big.Int, +) { + // create transaction params + params := []string{ + hex.EncodeToString([]byte(token.MvxUniversalToken)), + hex.EncodeToString(value.Bytes()), + hex.EncodeToString([]byte(createTransactionFunction)), + hex.EncodeToString(to.EthAddress.Bytes()), + } + dataField := strings.Join(params, "@") + + hash, txResult := handler.ChainSimulator.ScCall( + ctx, + from.MvxSk, + handler.SafeAddress, + zeroStringValue, + createDepositGasLimit+gasLimitPerDataByte*uint64(len(dataField)), + esdtTransferFunction, + params, + ) + log.Info("MultiversX->Ethereum createTransaction sent", "hash", hash, "token", token.MvxUniversalToken, "status", txResult.Status) +} + +func (handler *MultiversxHandler) unwrapCreateTransaction(ctx context.Context, token *TokenData, from KeysHolder, to KeysHolder, value *big.Int) { // create transaction params params := []string{ hex.EncodeToString([]byte(token.MvxUniversalToken)), hex.EncodeToString(value.Bytes()), hex.EncodeToString([]byte(unwrapTokenCreateTransactionFunction)), hex.EncodeToString([]byte(token.MvxChainSpecificToken)), + hex.EncodeToString(handler.SafeAddress.Bytes()), hex.EncodeToString(to.EthAddress.Bytes()), } dataField := strings.Join(params, "@") @@ -975,7 +996,7 @@ func (handler *MultiversxHandler) SendDepositTransactionFromMultiversx(ctx conte esdtTransferFunction, params, ) - log.Info("MultiversX->Ethereum transaction sent", "hash", hash, "token", token.MvxUniversalToken, "status", txResult.Status) + log.Info("MultiversX->Ethereum unwrapCreateTransaction sent", "hash", hash, "token", token.MvxUniversalToken, "status", txResult.Status) } // TestWithdrawFees will try to withdraw the fees for the provided token from the safe contract to the owner diff --git a/integrationTests/relayers/slowTests/framework/testSetup.go b/integrationTests/relayers/slowTests/framework/testSetup.go index ce2c78a8..228f6192 100644 --- a/integrationTests/relayers/slowTests/framework/testSetup.go +++ b/integrationTests/relayers/slowTests/framework/testSetup.go @@ -650,7 +650,7 @@ func (setup *TestSetup) createDepositOnMultiversxForToken(from KeysHolder, to Ke } depositValue.Add(depositValue, operation.ValueToSendFromMvX) - setup.MultiversxHandler.SendDepositTransactionFromMultiversx(setup.Ctx, from, to, token, operation.ValueToSendFromMvX) + setup.MultiversxHandler.SendDepositTransactionFromMultiversx(setup.Ctx, from, to, token, params, operation.ValueToSendFromMvX) } return depositValue diff --git a/integrationTests/relayers/slowTests/startsFromEthereumEdgecaseFlow.go b/integrationTests/relayers/slowTests/startsFromEthereumEdgecaseFlow.go new file mode 100644 index 00000000..9e4efe10 --- /dev/null +++ b/integrationTests/relayers/slowTests/startsFromEthereumEdgecaseFlow.go @@ -0,0 +1,49 @@ +//go:build slow + +package slowTests + +import ( + "fmt" + "testing" + + "github.com/multiversx/mx-bridge-eth-go/integrationTests/relayers/slowTests/framework" +) + +type startsFromEthereumEdgecaseFlow struct { + testing.TB + setup *framework.TestSetup + ethToMvxDone bool + mvxToEthDone bool + tokens []framework.TestTokenParams +} + +func (flow *startsFromEthereumEdgecaseFlow) process() (finished bool) { + if len(flow.tokens) == 0 { + return true + } + if flow.mvxToEthDone && flow.ethToMvxDone { + return true + } + + isTransferDoneFromEthereum := flow.setup.IsTransferDoneFromEthereum(flow.setup.AliceKeys, flow.setup.BobKeys, flow.tokens...) + if !flow.ethToMvxDone && isTransferDoneFromEthereum { + flow.ethToMvxDone = true + log.Info(fmt.Sprintf(framework.LogStepMarker, "Ethereum->MultiversX transfer finished, now sending back to Ethereum & another round from Ethereum...")) + + flow.setup.SendFromMultiversxToEthereum(flow.setup.BobKeys, flow.setup.AliceKeys, flow.tokens...) + flow.setup.SendFromEthereumToMultiversX(flow.setup.AliceKeys, flow.setup.BobKeys, flow.setup.MultiversxHandler.CalleeScAddress, flow.tokens...) + } + if !flow.ethToMvxDone { + // return here, no reason to check downwards + return false + } + + isTransferDoneFromMultiversX := flow.setup.IsTransferDoneFromMultiversX(flow.setup.BobKeys, flow.setup.AliceKeys, flow.tokens...) + if !flow.mvxToEthDone && isTransferDoneFromMultiversX { + flow.mvxToEthDone = true + log.Info(fmt.Sprintf(framework.LogStepMarker, "MultiversX<->Ethereum from Ethereum transfers done")) + return true + } + + return false +} diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.abi.json b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.abi.json index af0ec960..7b4939af 100644 --- a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.abi.json +++ b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.abi.json @@ -61,12 +61,6 @@ ], "outputs": [] }, - { - "name": "updateLowestTxId", - "mutability": "mutable", - "inputs": [], - "outputs": [] - }, { "name": "getPendingTransactionById", "mutability": "readonly", @@ -163,7 +157,7 @@ ] }, { - "name": "lowestTxId", + "name": "highestTxId", "mutability": "readonly", "inputs": [], "outputs": [ diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.wasm b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.wasm index c32618c4..57d10bd9 100755 Binary files a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.wasm and b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridge-proxy.wasm differ diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.abi.json b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.abi.json index 1287c24b..4be70732 100644 --- a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.abi.json +++ b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.abi.json @@ -168,6 +168,10 @@ "name": "requested_token", "type": "TokenIdentifier" }, + { + "name": "safe_address", + "type": "Address" + }, { "name": "to", "type": "EthAddress" @@ -175,19 +179,6 @@ ], "outputs": [] }, - { - "name": "setEsdtSafeContractAddress", - "onlyOwner": true, - "mutability": "mutable", - "inputs": [ - { - "name": "opt_new_address", - "type": "optional
", - "multi_arg": true - } - ], - "outputs": [] - }, { "name": "getUniversalBridgedTokenIds", "mutability": "readonly", @@ -245,16 +236,6 @@ } ] }, - { - "name": "getEsdtSafeContractAddress", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "Address" - } - ] - }, { "name": "pause", "onlyOwner": true, diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.wasm b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.wasm index b261b427..99dd4096 100755 Binary files a/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.wasm and b/integrationTests/relayers/slowTests/testdata/contracts/mvx/bridged-tokens-wrapper.wasm differ diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/esdt-safe.wasm b/integrationTests/relayers/slowTests/testdata/contracts/mvx/esdt-safe.wasm index 2217c142..7e179467 100755 Binary files a/integrationTests/relayers/slowTests/testdata/contracts/mvx/esdt-safe.wasm and b/integrationTests/relayers/slowTests/testdata/contracts/mvx/esdt-safe.wasm differ diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/multi-transfer-esdt.wasm b/integrationTests/relayers/slowTests/testdata/contracts/mvx/multi-transfer-esdt.wasm index d6de789b..15a73562 100755 Binary files a/integrationTests/relayers/slowTests/testdata/contracts/mvx/multi-transfer-esdt.wasm and b/integrationTests/relayers/slowTests/testdata/contracts/mvx/multi-transfer-esdt.wasm differ diff --git a/integrationTests/relayers/slowTests/testdata/contracts/mvx/multisig.wasm b/integrationTests/relayers/slowTests/testdata/contracts/mvx/multisig.wasm index 1d41892e..ddc0cd82 100755 Binary files a/integrationTests/relayers/slowTests/testdata/contracts/mvx/multisig.wasm and b/integrationTests/relayers/slowTests/testdata/contracts/mvx/multisig.wasm differ diff --git a/testsCommon/bridge/erc20ContractsHolderStub.go b/testsCommon/bridge/erc20ContractsHolderStub.go index 35cffe97..ba52db19 100644 --- a/testsCommon/bridge/erc20ContractsHolderStub.go +++ b/testsCommon/bridge/erc20ContractsHolderStub.go @@ -10,6 +10,7 @@ import ( // ERC20ContractsHolderStub - type ERC20ContractsHolderStub struct { BalanceOfCalled func(ctx context.Context, erc20Address common.Address, address common.Address) (*big.Int, error) + DecimalsCalled func(ctx context.Context, erc20Address common.Address) (uint8, error) } // BalanceOf - @@ -21,6 +22,15 @@ func (stub *ERC20ContractsHolderStub) BalanceOf(ctx context.Context, erc20Addres return big.NewInt(0), nil } +// Decimals - +func (stub *ERC20ContractsHolderStub) Decimals(ctx context.Context, erc20Address common.Address) (uint8, error) { + if stub.DecimalsCalled != nil { + return stub.DecimalsCalled(ctx, erc20Address) + } + + return 0, nil +} + // IsInterfaceNil - func (stub *ERC20ContractsHolderStub) IsInterfaceNil() bool { return stub == nil diff --git a/testsCommon/bridge/multiversxClientStub.go b/testsCommon/bridge/multiversxClientStub.go index fd115f6a..aeba233c 100644 --- a/testsCommon/bridge/multiversxClientStub.go +++ b/testsCommon/bridge/multiversxClientStub.go @@ -38,6 +38,7 @@ type MultiversXClientStub struct { MintBalancesCalled func(ctx context.Context, token []byte) (*big.Int, error) BurnBalancesCalled func(ctx context.Context, token []byte) (*big.Int, error) CheckRequiredBalanceCalled func(ctx context.Context, token []byte, value *big.Int) error + GetLastMvxBatchIDCalled func(ctx context.Context) (uint64, error) CloseCalled func() error } @@ -260,6 +261,15 @@ func (stub *MultiversXClientStub) CheckRequiredBalance(ctx context.Context, toke return nil } +// GetLastMvxBatchID - +func (stub *MultiversXClientStub) GetLastMvxBatchID(ctx context.Context) (uint64, error) { + if stub.GetLastMvxBatchIDCalled != nil { + return stub.GetLastMvxBatchIDCalled(ctx) + } + + return 0, nil +} + // Close - func (stub *MultiversXClientStub) Close() error { if stub.CloseCalled != nil { diff --git a/testsCommon/interactors/genericErc20ContractStub.go b/testsCommon/interactors/genericErc20ContractStub.go index 77495927..e9856a21 100644 --- a/testsCommon/interactors/genericErc20ContractStub.go +++ b/testsCommon/interactors/genericErc20ContractStub.go @@ -11,6 +11,7 @@ import ( // GenericErc20ContractStub - type GenericErc20ContractStub struct { BalanceOfCalled func(account common.Address) (*big.Int, error) + DecimalsCalled func() (uint8, error) } // BalanceOf - @@ -21,3 +22,12 @@ func (stub *GenericErc20ContractStub) BalanceOf(_ *bind.CallOpts, account common return nil, errors.New("GenericErc20ContractStub.BalanceOf not implemented") } + +// Decimals - +func (stub *GenericErc20ContractStub) Decimals(_ *bind.CallOpts) (uint8, error) { + if stub.DecimalsCalled != nil { + return stub.DecimalsCalled() + } + + return 0, errors.New("GenericErc20ContractStub.Decimals not implemented") +}