diff --git a/x/crosschain/keeper/abci.go b/x/crosschain/keeper/abci.go index d18648e6e4..b5bb5dff01 100644 --- a/x/crosschain/keeper/abci.go +++ b/x/crosschain/keeper/abci.go @@ -52,8 +52,18 @@ func (k Keeper) IterateAndUpdateCctxGasPrice( IterateChains: for _, chain := range chains { + if zetachains.IsZetaChain(chain.ChainId, additionalChains) { + continue + } + // support only external evm chains and bitcoin chain - if IsGasStabilityPoolEnabledChain(chain.ChainId, additionalChains) { + // use provided updateFunc if available, otherwise get updater based on chain type + updater, found := GetCctxGasPriceUpdater(chain.ChainId, additionalChains) + if found && updateFunc == nil { + updateFunc = updater + } + + if updateFunc != nil { res, err := k.ListPendingCctx(sdk.UnwrapSDKContext(ctx), &types.QueryListPendingCctxRequest{ ChainId: chain.ChainId, Limit: gasPriceIncreaseFlags.MaxPendingCctxs, @@ -100,9 +110,9 @@ IterateChains: return cctxCount, gasPriceIncreaseFlags } -// CheckAndUpdateCctxGasPrice checks if the retry interval is reached and updates the gas price if so +// CheckAndUpdateCctxGasPriceEVM checks if the retry interval is reached and updates the gas price if so // The function returns the gas price increase and the additional fees paid from the gas stability pool -func CheckAndUpdateCctxGasPrice( +func CheckAndUpdateCctxGasPriceEVM( ctx sdk.Context, k Keeper, cctx types.CrossChainTx, @@ -176,14 +186,54 @@ func CheckAndUpdateCctxGasPrice( return gasPriceIncrease, additionalFees, nil } -// IsGasStabilityPoolEnabledChain returns true if given chainID is enabled for gas stability pool -func IsGasStabilityPoolEnabledChain(chainID int64, additionalChains []zetachains.Chain) bool { +// CheckAndUpdateCctxGasRateBTC checks if the retry interval is reached and updates the gas rate if so +// Zetacore only needs to update the gas rate in CCTX and fee bumping will be handled by zetaclient +func CheckAndUpdateCctxGasRateBTC( + ctx sdk.Context, + k Keeper, + cctx types.CrossChainTx, + flags observertypes.GasPriceIncreaseFlags, +) (math.Uint, math.Uint, error) { + // skip if gas price or gas limit is not set + if cctx.GetCurrentOutboundParam().GasPrice == "" || cctx.GetCurrentOutboundParam().CallOptions.GasLimit == 0 { + return math.ZeroUint(), math.ZeroUint(), nil + } + + // skip if retry interval is not reached + lastUpdated := time.Unix(cctx.CctxStatus.LastUpdateTimestamp, 0) + if ctx.BlockTime().Before(lastUpdated.Add(flags.RetryInterval)) { + return math.ZeroUint(), math.ZeroUint(), nil + } + + // compute gas price increase + chainID := cctx.GetCurrentOutboundParam().ReceiverChainId + medianGasPrice, _, isFound := k.GetMedianGasValues(ctx, chainID) + if !isFound { + return math.ZeroUint(), math.ZeroUint(), cosmoserrors.Wrap( + types.ErrUnableToGetGasPrice, + fmt.Sprintf("cannot get gas price for chain %d", chainID), + ) + } + + // set new gas rate and last update timestamp + // there is no priority fee in Bitcoin, we reuse 'GasPriorityFee' to store latest gas rate in satoshi/vByte + cctx.GetCurrentOutboundParam().GasPriorityFee = medianGasPrice.String() + k.SetCrossChainTx(ctx, cctx) + + return math.ZeroUint(), math.ZeroUint(), nil +} + +// GetCctxGasPriceUpdater returns the function to update gas price according to chain type +func GetCctxGasPriceUpdater(chainID int64, additionalChains []zetachains.Chain) (CheckAndUpdateCctxGasPriceFunc, bool) { switch { case zetachains.IsEVMChain(chainID, additionalChains): - return !zetachains.IsZetaChain(chainID, additionalChains) + if !zetachains.IsZetaChain(chainID, additionalChains) { + return CheckAndUpdateCctxGasPriceEVM, true + } + return nil, false case zetachains.IsBitcoinChain(chainID, additionalChains): - return true + return CheckAndUpdateCctxGasRateBTC, true default: - return false + return nil, false } } diff --git a/x/crosschain/keeper/abci_test.go b/x/crosschain/keeper/abci_test.go index 2e1cbd246b..5db8261904 100644 --- a/x/crosschain/keeper/abci_test.go +++ b/x/crosschain/keeper/abci_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "errors" + "reflect" "testing" "time" @@ -88,15 +89,21 @@ func TestKeeper_IterateAndUpdateCctxGasPrice(t *testing.T) { ctx = ctx.WithBlockHeight(observertypes.DefaultCrosschainFlags().GasPriceIncreaseFlags.EpochLength * 2) cctxCount, flags = k.IterateAndUpdateCctxGasPrice(ctx, supportedChains, updateFunc) - // 2 eth + 5 bsc = 7 - require.Equal(t, 7, cctxCount) + // 2 eth + 5 btc + 5 bsc = 12 + require.Equal(t, 12, cctxCount) require.Equal(t, customFlags, flags) // check that the update function was called with the cctx index - require.Equal(t, 7, len(updateFuncMap)) + require.Equal(t, 12, len(updateFuncMap)) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("1-10")) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("1-11")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-20")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-21")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-22")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-23")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("8332-24")) + require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-30")) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-31")) require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-32")) @@ -104,7 +111,7 @@ func TestKeeper_IterateAndUpdateCctxGasPrice(t *testing.T) { require.Contains(t, updateFuncMap, sample.GetCctxIndexFromString("56-34")) } -func TestCheckAndUpdateCctxGasPrice(t *testing.T) { +func Test_CheckAndUpdateCctxGasPriceEVM(t *testing.T) { sampleTimestamp := time.Now() retryIntervalReached := sampleTimestamp.Add(observertypes.DefaultGasPriceIncreaseFlags.RetryInterval + time.Second) retryIntervalNotReached := sampleTimestamp.Add( @@ -407,7 +414,7 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { } // check and update gas price - gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCctxGasPrice(ctx, *k, tc.cctx, tc.flags) + gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCctxGasPriceEVM(ctx, *k, tc.cctx, tc.flags) if tc.isError { require.Error(t, err) @@ -451,47 +458,219 @@ func TestCheckAndUpdateCctxGasPrice(t *testing.T) { } } -func TestIsGasStabilityPoolEnabledChain(t *testing.T) { +func Test_CheckAndUpdateCctxGasRateBTC(t *testing.T) { + sampleTimestamp := time.Now() + gasRateUpdateInterval := observertypes.DefaultGasPriceIncreaseFlags.RetryInterval + retryIntervalReached := sampleTimestamp.Add(gasRateUpdateInterval + time.Second) + retryIntervalNotReached := sampleTimestamp.Add(gasRateUpdateInterval - time.Second) + + tt := []struct { + name string + cctx types.CrossChainTx + flags observertypes.GasPriceIncreaseFlags + blockTimestamp time.Time + medianGasPrice uint64 + medianPriorityFee uint64 + shouldUpdate bool + isError bool + }{ + { + name: "can update gas rate when retry interval is reached", + cctx: types.CrossChainTx{ + Index: "a", + CctxStatus: &types.Status{ + CreatedTimestamp: sampleTimestamp.Unix(), + LastUpdateTimestamp: sampleTimestamp.Unix(), + }, + OutboundParams: []*types.OutboundParams{ + { + ReceiverChainId: 8332, + CallOptions: &types.CallOptions{ + GasLimit: 254, + }, + GasPrice: "10", + }, + }, + }, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalReached, + medianGasPrice: 12, + shouldUpdate: true, + }, + { + name: "skip if gas price is not set", + cctx: types.CrossChainTx{ + Index: "b1", + OutboundParams: []*types.OutboundParams{ + { + GasPrice: "", + }, + }, + }, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalReached, + medianGasPrice: 12, + }, + { + name: "skip if gas limit is not set", + cctx: types.CrossChainTx{ + Index: "b2", + OutboundParams: []*types.OutboundParams{ + { + CallOptions: &types.CallOptions{ + GasLimit: 0, + }, + GasPrice: "10", + }, + }, + }, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalReached, + medianGasPrice: 12, + }, + { + name: "skip if retry interval is not reached", + cctx: types.CrossChainTx{ + Index: "b3", + CctxStatus: &types.Status{ + CreatedTimestamp: sampleTimestamp.Unix(), + LastUpdateTimestamp: sampleTimestamp.Unix(), + }, + OutboundParams: []*types.OutboundParams{ + { + CallOptions: &types.CallOptions{ + GasLimit: 254, + }, + GasPrice: "10", + }, + }, + }, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalNotReached, + medianGasPrice: 12, + }, + { + name: "returns error if can't find median gas price", + cctx: types.CrossChainTx{ + Index: "b4", + CctxStatus: &types.Status{ + CreatedTimestamp: sampleTimestamp.Unix(), + LastUpdateTimestamp: sampleTimestamp.Unix(), + }, + OutboundParams: []*types.OutboundParams{ + { + ReceiverChainId: 8332, + CallOptions: &types.CallOptions{ + GasLimit: 254, + }, + GasPrice: "10", + }, + }, + }, + flags: observertypes.DefaultGasPriceIncreaseFlags, + blockTimestamp: retryIntervalReached, + medianGasPrice: 0, + isError: true, + }, + } + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + k, ctx := testkeeper.CrosschainKeeperAllMocks(t) + chainID := tc.cctx.GetCurrentOutboundParam().ReceiverChainId + + // set median gas price if not zero + if tc.medianGasPrice != 0 { + k.SetGasPrice(ctx, types.GasPrice{ + ChainId: chainID, + Prices: []uint64{tc.medianGasPrice}, + MedianIndex: 0, + }) + + // ensure median gas price is set + medianGasPrice, _, isFound := k.GetMedianGasValues(ctx, chainID) + require.True(t, isFound) + require.True(t, medianGasPrice.Equal(math.NewUint(tc.medianGasPrice))) + } + + // set block timestamp + ctx = ctx.WithBlockTime(tc.blockTimestamp) + + // check and update gas rate + gasPriceIncrease, feesPaid, err := keeper.CheckAndUpdateCctxGasRateBTC(ctx, *k, tc.cctx, tc.flags) + if tc.isError { + require.Error(t, err) + return + } + require.NoError(t, err) + + // check values + require.True(t, gasPriceIncrease.IsZero()) + require.True(t, feesPaid.IsZero()) + + // check cctx if gas rate is updated + if tc.shouldUpdate { + cctx, found := k.GetCrossChainTx(ctx, tc.cctx.Index) + require.True(t, found) + newGasPrice, err := cctx.GetCurrentOutboundParam().GetGasPriorityFeeUInt64() + require.NoError(t, err) + require.Equal(t, tc.medianGasPrice, newGasPrice) + require.EqualValues(t, tc.blockTimestamp.Unix(), cctx.CctxStatus.LastUpdateTimestamp) + } + }) + } +} + +func Test_GetCctxGasPriceUpdater(t *testing.T) { tests := []struct { - name string - chainID int64 - expected bool + name string + chainID int64 + found bool + updateFunc keeper.CheckAndUpdateCctxGasPriceFunc }{ { - name: "Ethereum is enabled", - chainID: chains.Ethereum.ChainId, - expected: true, + name: "Ethereum is enabled", + chainID: chains.Ethereum.ChainId, + found: true, + updateFunc: keeper.CheckAndUpdateCctxGasPriceEVM, }, { - name: "Binance Smart Chain is enabled", - chainID: chains.BscMainnet.ChainId, - expected: true, + name: "Binance Smart Chain is enabled", + chainID: chains.BscMainnet.ChainId, + found: true, + updateFunc: keeper.CheckAndUpdateCctxGasPriceEVM, }, { - name: "Bitcoin is enabled", - chainID: chains.BitcoinMainnet.ChainId, - expected: true, + name: "Bitcoin is enabled", + chainID: chains.BitcoinMainnet.ChainId, + found: true, + updateFunc: keeper.CheckAndUpdateCctxGasRateBTC, }, { - name: "ZetaChain is not enabled", - chainID: chains.ZetaChainMainnet.ChainId, - expected: false, + name: "ZetaChain is not enabled", + chainID: chains.ZetaChainMainnet.ChainId, + found: false, + updateFunc: nil, }, { - name: "Solana is not enabled", - chainID: chains.SolanaMainnet.ChainId, - expected: false, + name: "Solana is not enabled", + chainID: chains.SolanaMainnet.ChainId, + found: false, + updateFunc: nil, }, { - name: "TON is not enabled", - chainID: chains.TONMainnet.ChainId, - expected: false, + name: "TON is not enabled", + chainID: chains.TONMainnet.ChainId, + found: false, + updateFunc: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, keeper.IsGasStabilityPoolEnabledChain(tt.chainID, []zetachains.Chain{})) + updateFunc, found := keeper.GetCctxGasPriceUpdater(tt.chainID, []zetachains.Chain{}) + require.Equal(t, tt.found, found) + require.Equal(t, reflect.ValueOf(tt.updateFunc).Pointer(), reflect.ValueOf(updateFunc).Pointer()) }) } } diff --git a/x/crosschain/module.go b/x/crosschain/module.go index e3b71cfeb0..743043c190 100644 --- a/x/crosschain/module.go +++ b/x/crosschain/module.go @@ -172,7 +172,7 @@ func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { // iterate and update gas price for cctx that are pending for too long // error is logged in the function - am.keeper.IterateAndUpdateCctxGasPrice(ctx, supportedChains, keeper.CheckAndUpdateCctxGasPrice) + am.keeper.IterateAndUpdateCctxGasPrice(ctx, supportedChains, nil) } // EndBlock executes all ABCI EndBlock logic respective to the crosschain module. It