diff --git a/x/interchainquery/types/keys.go b/x/interchainquery/types/keys.go index 03de143c06..7da8303e94 100644 --- a/x/interchainquery/types/keys.go +++ b/x/interchainquery/types/keys.go @@ -54,6 +54,11 @@ func KeyPrefix(p string) []byte { } func FormatOsmosisMostRecentTWAPKey(poolId uint64, denom1, denom2 string) []byte { + // Sort denoms + if denom1 > denom2 { + denom1, denom2 = denom2, denom1 + } + poolIdBz := fmt.Sprintf("%0.20d", poolId) return []byte(fmt.Sprintf("%s%s%s%s%s%s", OsmosisMostRecentTWAPsPrefix, poolIdBz, OsmosisKeySeparator, denom1, OsmosisKeySeparator, denom2)) } diff --git a/x/stakeibc/keeper/keeper_test.go b/x/stakeibc/keeper/keeper_test.go index ae379391b7..3e531def7e 100644 --- a/x/stakeibc/keeper/keeper_test.go +++ b/x/stakeibc/keeper/keeper_test.go @@ -5,12 +5,14 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + ibctesting "github.com/cosmos/ibc-go/v7/testing" "github.com/stretchr/testify/suite" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/Stride-Labs/stride/v16/app/apptesting" + icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) @@ -81,6 +83,40 @@ func (s *KeeperTestSuite) CreateEpochForICATimeout(epochType string, timeoutDura s.App.StakeibcKeeper.SetEpochTracker(s.Ctx, epochTracker) } +// Validates the query object stored after an ICQ submission, using some default testing +// values (e.g. HostChainId, stakeibc module name, etc.), and returning the query +// NOTE: This assumes there was only one submission and grabs the first query from the store +func (s *KeeperTestSuite) ValidateQuerySubmission( + queryType string, + queryData []byte, + callbackId string, + timeoutDuration time.Duration, + timeoutPolicy icqtypes.TimeoutPolicy, +) icqtypes.Query { + // Check that there's only one query + queries := s.App.InterchainqueryKeeper.AllQueries(s.Ctx) + s.Require().Len(queries, 1, "there should have been 1 query submitted") + query := queries[0] + + // Validate the chainId and connectionId + s.Require().Equal(HostChainId, query.ChainId, "query chain ID") + s.Require().Equal(ibctesting.FirstConnectionID, query.ConnectionId, "query connection ID") + s.Require().Equal(types.ModuleName, query.CallbackModule, "query module") + + // Validate the query type and request data + s.Require().Equal(queryType, query.QueryType, "query type") + s.Require().Equal(string(queryData), string(query.RequestData), "query request data") + s.Require().Equal(callbackId, query.CallbackId, "query callback ID") + + // Validate the query timeout + expectedTimeoutTimestamp := s.Ctx.BlockTime().Add(timeoutDuration).UnixNano() + s.Require().Equal(timeoutDuration, query.TimeoutDuration, "query timeout duration") + s.Require().Equal(expectedTimeoutTimestamp, int64(query.TimeoutTimestamp), "query timeout timestamp") + s.Require().Equal(icqtypes.TimeoutPolicy_REJECT_QUERY_RESPONSE, query.TimeoutPolicy, "query timeout policy") + + return query +} + func (s *KeeperTestSuite) TestIsRedemptionRateWithinSafetyBounds() { params := s.App.StakeibcKeeper.GetParams(s.Ctx) params.DefaultMinRedemptionRateThreshold = 75 diff --git a/x/stakeibc/keeper/reward_converter.go b/x/stakeibc/keeper/reward_converter.go index f2375e5dc7..d6d295fb88 100644 --- a/x/stakeibc/keeper/reward_converter.go +++ b/x/stakeibc/keeper/reward_converter.go @@ -461,14 +461,12 @@ func (k Keeper) PoolPriceQuery(ctx sdk.Context, route types.TradeRoute) error { tradeAccount := route.TradeAccount k.Logger(ctx).Info(utils.LogWithHostZone(tradeAccount.ChainId, "Submitting ICQ for spot price in this pool")) - // Sort denom's - denom1, denom2 := route.RewardDenomOnTradeZone, route.HostDenomOnTradeZone - if denom1 > denom2 { - denom1, denom2 = denom2, denom1 - } - // Build query request data which consists of the TWAP store key built from each denom - queryData := icqtypes.FormatOsmosisMostRecentTWAPKey(route.TradeConfig.PoolId, denom1, denom2) + queryData := icqtypes.FormatOsmosisMostRecentTWAPKey( + route.TradeConfig.PoolId, + route.RewardDenomOnTradeZone, + route.HostDenomOnTradeZone, + ) // Timeout query at end of epoch hourEpochTracker, found := k.GetEpochTracker(ctx, epochstypes.HOUR_EPOCH) diff --git a/x/stakeibc/keeper/reward_converter_test.go b/x/stakeibc/keeper/reward_converter_test.go index cd216c1489..9ed5a16334 100644 --- a/x/stakeibc/keeper/reward_converter_test.go +++ b/x/stakeibc/keeper/reward_converter_test.go @@ -6,10 +6,13 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" ibctesting "github.com/cosmos/ibc-go/v7/testing" epochtypes "github.com/Stride-Labs/stride/v16/x/epochs/types" + icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" + "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper" "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) @@ -419,3 +422,72 @@ func (s *KeeperTestSuite) TestSwapRewardTokens() { err = s.App.StakeibcKeeper.SwapRewardTokens(s.Ctx, rewardAmount, route) s.Require().ErrorContains(err, "epoch not found") } + +func (s *KeeperTestSuite) TestPoolPriceQuery() { + // Create a transfer channel so the connection exists for the query submission + s.CreateTransferChannel(HostChainId) + + // Create an epoch tracker to dictate the query timeout + timeoutDuration := time.Minute * 10 + s.CreateEpochForICATimeout(epochtypes.HOUR_EPOCH, timeoutDuration) + + // Define the trade route + poolId := uint64(100) + tradeRewardDenom := "ibc/reward-denom-on-trade" + tradeHostDenom := "ibc/reward-denom-on-host" + + route := types.TradeRoute{ + RewardDenomOnRewardZone: RewardDenom, + HostDenomOnHostZone: HostDenom, + RewardDenomOnTradeZone: tradeRewardDenom, + HostDenomOnTradeZone: tradeHostDenom, + + TradeAccount: types.ICAAccount{ + ChainId: HostChainId, + ConnectionId: ibctesting.FirstConnectionID, + }, + TradeConfig: types.TradeConfig{ + PoolId: poolId, + }, + } + + expectedCallbackData := types.TradeRouteCallback{ + RewardDenom: RewardDenom, + HostDenom: HostDenom, + } + + // Submit the pool price ICQ + err := s.App.StakeibcKeeper.PoolPriceQuery(s.Ctx, route) + s.Require().NoError(err, "no error expected when submitting pool price query") + + // Confirm the query request key is the same regardless of which order the denom's are specified + expectedRequestData := icqtypes.FormatOsmosisMostRecentTWAPKey(poolId, tradeRewardDenom, tradeHostDenom) + expectedRequestDataSwapped := icqtypes.FormatOsmosisMostRecentTWAPKey(poolId, tradeHostDenom, tradeRewardDenom) + s.Require().Equal(expectedRequestData, expectedRequestDataSwapped, "osmosis twap denoms should be sorted") + + // Validate the fields of the query + query := s.ValidateQuerySubmission( + icqtypes.TWAP_STORE_QUERY_WITH_PROOF, + expectedRequestData, + keeper.ICQCallbackID_PoolPrice, + timeoutDuration, + icqtypes.TimeoutPolicy_REJECT_QUERY_RESPONSE, + ) + + // Validate the query callback data + var actualCallbackData types.TradeRouteCallback + err = proto.Unmarshal(query.CallbackData, &actualCallbackData) + s.Require().NoError(err) + s.Require().Equal(expectedCallbackData, actualCallbackData, "query callback data") + + // Remove the connection ID from the trade account and confirm the query submission fails + invalidRoute := route + invalidRoute.TradeAccount.ConnectionId = "" + err = s.App.StakeibcKeeper.PoolPriceQuery(s.Ctx, invalidRoute) + s.Require().ErrorContains(err, "invalid interchain query request") + + // Remove the epoch tracker so the function fails to get a timeout + s.App.StakeibcKeeper.RemoveEpochTracker(s.Ctx, epochtypes.HOUR_EPOCH) + err = s.App.StakeibcKeeper.PoolPriceQuery(s.Ctx, route) + s.Require().ErrorContains(err, "hour: epoch not found") +}