diff --git a/app/apptesting/test_helpers.go b/app/apptesting/test_helpers.go index 941164717f..9afe804c41 100644 --- a/app/apptesting/test_helpers.go +++ b/app/apptesting/test_helpers.go @@ -67,7 +67,6 @@ func (s *AppTestHelper) Setup() { s.TestAccs = CreateRandomAccounts(3) s.IbcEnabled = false s.IcaAddresses = make(map[string]string) - } // Mints coins directly to a module account @@ -138,6 +137,7 @@ func (s *AppTestHelper) CreateTransferChannel(hostChainID string) { s.App = s.StrideChain.App.(*app.StrideApp) s.HostApp = s.HostChain.GetSimApp() s.Ctx = s.StrideChain.GetContext() + // Finally confirm the channel was setup properly s.Require().Equal(ibctesting.FirstClientID, s.TransferPath.EndpointA.ClientID, "stride clientID") s.Require().Equal(ibctesting.FirstConnectionID, s.TransferPath.EndpointA.ConnectionID, "stride connectionID") @@ -166,7 +166,6 @@ func (s *AppTestHelper) CreateICAChannel(owner string) string { icaPath = CopyConnectionAndClientToPath(icaPath, s.TransferPath) // Register the ICA and complete the handshake - s.RegisterInterchainAccount(icaPath.EndpointA, owner) err := icaPath.EndpointB.ChanOpenTry() @@ -179,6 +178,7 @@ func (s *AppTestHelper) CreateICAChannel(owner string) string { s.Require().NoError(err, "ChanOpenConfirm error") s.Ctx = s.StrideChain.GetContext() + // Confirm the ICA channel was created properly portID := icaPath.EndpointA.ChannelConfig.PortID channelID := icaPath.EndpointA.ChannelID diff --git a/app/upgrades.go b/app/upgrades.go index 829f2f9b72..fbb66279a2 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -37,7 +37,7 @@ func (app *StrideApp) setupUpgradeHandlers() { // v5 upgrade handler app.UpgradeKeeper.SetUpgradeHandler( v5.UpgradeName, - v5.CreateUpgradeHandler(app.mm, app.configurator, app.InterchainqueryKeeper), + v5.CreateUpgradeHandler(app.mm, app.configurator, app.InterchainqueryKeeper, app.StakeibcKeeper), ) upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() diff --git a/app/upgrades/v5/upgrades.go b/app/upgrades/v5/upgrades.go index 0b787bfb85..8e797ab3e8 100644 --- a/app/upgrades/v5/upgrades.go +++ b/app/upgrades/v5/upgrades.go @@ -7,6 +7,8 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" interchainquerykeeper "github.com/Stride-Labs/stride/v4/x/interchainquery/keeper" + stakeibckeeper "github.com/Stride-Labs/stride/v4/x/stakeibc/keeper" + stakeibctypes "github.com/Stride-Labs/stride/v4/x/stakeibc/types" ) // Note: ensure these values are properly set before running upgrade @@ -19,6 +21,7 @@ func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, interchainqueryKeeper interchainquerykeeper.Keeper, + stakeibcKeeper stakeibckeeper.Keeper, ) upgradetypes.UpgradeHandler { return func(ctx sdk.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { // Remove authz from store as it causes an issue with state sync @@ -29,6 +32,11 @@ func CreateUpgradeHandler( staleQueryId := "60b8e09dc7a65938cd6e6e5728b8aa0ca3726ffbe5511946a4f08ced316174ab" interchainqueryKeeper.DeleteQuery(ctx, staleQueryId) + // Add the SafetyMaxSlashPercent param to the stakeibc param store + stakeibcParams := stakeibcKeeper.GetParams(ctx) + stakeibcParams.SafetyMaxSlashPercent = stakeibctypes.DefaultSafetyMaxSlashPercent + stakeibcKeeper.SetParams(ctx, stakeibcParams) + return mm.RunMigrations(ctx, configurator, vm) } } diff --git a/dockernet/config.sh b/dockernet/config.sh index 67831c64c1..27f7db5199 100644 --- a/dockernet/config.sh +++ b/dockernet/config.sh @@ -16,10 +16,11 @@ TX_LOGS=$DOCKERNET_HOME/logs/tx.log KEYS_LOGS=$DOCKERNET_HOME/logs/keys.log # List of hosts enabled -# `start-docker` defaults to just GAIA if HOST_CHAINS is empty -# `start-docker-all` always runs all hosts HOST_CHAINS=() +# If no host zones are specified above: +# `start-docker` defaults to just GAIA if HOST_CHAINS is empty +# `start-docker-all` always runs all hosts if [[ "${ALL_HOST_CHAINS:-false}" == "true" ]]; then HOST_CHAINS=(GAIA JUNO OSMO STARS) elif [[ "${#HOST_CHAINS[@]}" == "0" ]]; then diff --git a/dockernet/src/init_chain.sh b/dockernet/src/init_chain.sh index 46e53791a7..ca1b1cfd0b 100644 --- a/dockernet/src/init_chain.sh +++ b/dockernet/src/init_chain.sh @@ -51,7 +51,7 @@ set_host_genesis() { # This makes it easier to test updating weights after a host zone validator is slashed sed -i -E 's|"signed_blocks_window": "100"|"signed_blocks_window": "10"|g' $genesis_config sed -i -E 's|"downtime_jail_duration": "600s"|"downtime_jail_duration": "10s"|g' $genesis_config - sed -i -E 's|"slash_fraction_downtime": "0.010000000000000000"|"slash_fraction_downtime": "0.100000000000000000"|g' $genesis_config + sed -i -E 's|"slash_fraction_downtime": "0.010000000000000000"|"slash_fraction_downtime": "0.050000000000000000"|g' $genesis_config } MAIN_ID=1 # Node responsible for genesis and persistent_peers diff --git a/proto/stride/stakeibc/params.proto b/proto/stride/stakeibc/params.proto index 0b358fc200..5b55a53642 100755 --- a/proto/stride/stakeibc/params.proto +++ b/proto/stride/stakeibc/params.proto @@ -33,4 +33,5 @@ message Params { uint64 safety_max_redemption_rate_threshold = 15; uint64 ibc_transfer_timeout_nanos = 16; uint64 safety_num_validators = 17; + uint64 safety_max_slash_percent = 18; } \ No newline at end of file diff --git a/x/icacallbacks/keeper/keeper.go b/x/icacallbacks/keeper/keeper.go index 76d4cda84a..e5bac82030 100644 --- a/x/icacallbacks/keeper/keeper.go +++ b/x/icacallbacks/keeper/keeper.go @@ -148,8 +148,6 @@ func (k Keeper) CallRegisteredICACallback(ctx sdk.Context, modulePacket channelt } else { k.Logger(ctx).Error(fmt.Sprintf("Callback %v has no associated callback", callbackData)) } - // QUESTION: Do we want to catch the case where the callback ID has not been registered? - // Maybe just as an info log if it's expected that some acks do not have an associated callback? // remove the callback data k.RemoveCallbackData(ctx, callbackDataKey) diff --git a/x/interchainquery/keeper/msg_server.go b/x/interchainquery/keeper/msg_server.go index 2a78af2b9c..d6150ab159 100644 --- a/x/interchainquery/keeper/msg_server.go +++ b/x/interchainquery/keeper/msg_server.go @@ -2,7 +2,6 @@ package keeper import ( "context" - "fmt" "net/url" "sort" "strings" @@ -14,6 +13,7 @@ import ( tmclienttypes "github.com/cosmos/ibc-go/v5/modules/light-clients/07-tendermint/types" "github.com/spf13/cast" + "github.com/Stride-Labs/stride/v4/utils" "github.com/Stride-Labs/stride/v4/x/interchainquery/types" ) @@ -30,130 +30,101 @@ func NewMsgServerImpl(keeper Keeper) types.MsgServer { var _ types.MsgServer = msgServer{} // check if the query requires proving; if it does, verify it! -func (k Keeper) VerifyKeyProof(ctx sdk.Context, msg *types.MsgSubmitQueryResponse, q types.Query) error { - pathParts := strings.Split(q.QueryType, "/") +func (k Keeper) VerifyKeyProof(ctx sdk.Context, msg *types.MsgSubmitQueryResponse, query types.Query) error { + pathParts := strings.Split(query.QueryType, "/") // the query does NOT have an associated proof, so no need to verify it. if pathParts[len(pathParts)-1] != "key" { return nil - } else { - // the query is a "key" proof query -- verify the results are valid by checking the proof! - if msg.ProofOps == nil { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, unable to validate proof. No proof submitted", q.Id) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) - } - connection, _ := k.IBCKeeper.ConnectionKeeper.GetConnection(ctx, q.ConnectionId) + } - msgHeight, err := cast.ToUint64E(msg.Height) - if err != nil { - return err - } - height := clienttypes.NewHeight(clienttypes.ParseChainID(q.ChainId), msgHeight+1) - consensusState, found := k.IBCKeeper.ClientKeeper.GetClientConsensusState(ctx, connection.ClientId, height) - if !found { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, consensus state not found for client %s and height %d", q.Id, connection.ClientId, height) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) - } + // If the query is a "key" proof query, verify the results are valid by checking the poof + if msg.ProofOps == nil { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Unable to validate proof. No proof submitted") + } - clientState, found := k.IBCKeeper.ClientKeeper.GetClientState(ctx, connection.ClientId) - if !found { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, unable to fetch client state for client %s and height %d", q.Id, connection.ClientId, height) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) - } - path := commitmenttypes.NewMerklePath([]string{pathParts[1], url.PathEscape(string(q.Request))}...) + // Get the client consensus state at the height 1 block above the message height + msgHeight, err := cast.ToUint64E(msg.Height) + if err != nil { + return err + } + height := clienttypes.NewHeight(clienttypes.ParseChainID(query.ChainId), msgHeight+1) - merkleProof, err := commitmenttypes.ConvertProofs(msg.ProofOps) - if err != nil { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, error converting proofs", q.Id) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) - } + // Get the client state and consensus state from the connection Id + connection, found := k.IBCKeeper.ConnectionKeeper.GetConnection(ctx, query.ConnectionId) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "ConnectionId %s does not exist", query.ConnectionId) + } + consensusState, found := k.IBCKeeper.ClientKeeper.GetClientConsensusState(ctx, connection.ClientId, height) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Consensus state not found for client %s and height %d", connection.ClientId, height) + } + clientState, found := k.IBCKeeper.ClientKeeper.GetClientState(ctx, connection.ClientId) + if !found { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Unable to fetch client state for client %s", connection.ClientId) + } + tmClientState, ok := clientState.(*tmclienttypes.ClientState) + if !ok { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Client state is not tendermint") + } - tmclientstate, ok := clientState.(*tmclienttypes.ClientState) - if !ok { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, error unmarshaling client state %v", q.Id, clientState) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) + // Get the merkle path and merkle proof + path := commitmenttypes.NewMerklePath([]string{pathParts[1], url.PathEscape(string(query.Request))}...) + merkleProof, err := commitmenttypes.ConvertProofs(msg.ProofOps) + if err != nil { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Error converting proofs: %s", err.Error()) + } + + // If we got a non-nil response, verify inclusion proof + if len(msg.Result) != 0 { + if err := merkleProof.VerifyMembership(tmClientState.ProofSpecs, consensusState.GetRoot(), path, msg.Result); err != nil { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Unable to verify membership proof: %s", err.Error()) } + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, query.CallbackId, "Inclusion proof validated - QueryId %s", query.Id)) - if len(msg.Result) != 0 { - // if we got a non-nil response, verify inclusion proof. - if err := merkleProof.VerifyMembership(tmclientstate.ProofSpecs, consensusState.GetRoot(), path, msg.Result); err != nil { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, unable to verify membership proof: %s", q.Id, err) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) - } - k.Logger(ctx).Info(fmt.Sprintf("Proof validated! module: %s, queryId %s", types.ModuleName, q.Id)) - - } else { - // if we got a nil response, verify non inclusion proof. - if err := merkleProof.VerifyNonMembership(tmclientstate.ProofSpecs, consensusState.GetRoot(), path); err != nil { - errMsg := fmt.Sprintf("[ICQ Resp] for query %s, unable to verify non-membership proof: %s", q.Id, err) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrInvalidICQProof, errMsg) - } - k.Logger(ctx).Info(fmt.Sprintf("Non-inclusion Proof validated, stopping here! module: %s, queryId %s", types.ModuleName, q.Id)) + } else { + // if we got a nil query response, verify non inclusion proof. + if err := merkleProof.VerifyNonMembership(tmClientState.ProofSpecs, consensusState.GetRoot(), path); err != nil { + return sdkerrors.Wrapf(types.ErrInvalidICQProof, "Unable to verify non-membership proof: %s", err.Error()) } + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, query.CallbackId, "Non-inclusion proof validated - QueryId %s", query.Id)) } + return nil } // call the query's associated callback function -func (k Keeper) InvokeCallback(ctx sdk.Context, msg *types.MsgSubmitQueryResponse, q types.Query) error { - // get all the stored queries and sort them for determinism +func (k Keeper) InvokeCallback(ctx sdk.Context, msg *types.MsgSubmitQueryResponse, query types.Query) error { + // get all the callback handlers and sort them for determinism + // (each module has their own callback handler) moduleNames := []string{} for moduleName := range k.callbacks { moduleNames = append(moduleNames, moduleName) } sort.Strings(moduleNames) + // Loop through each module until the callbackId is found in one of the module handlers for _, moduleName := range moduleNames { - k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] executing callback for queryId (%s), module (%s)", q.Id, moduleName)) moduleCallbackHandler := k.callbacks[moduleName] - if moduleCallbackHandler.HasICQCallback(q.CallbackId) { - k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] callback (%s) found for module (%s)", q.CallbackId, moduleName)) - // call the correct callback function - err := moduleCallbackHandler.CallICQCallback(ctx, q.CallbackId, msg.Result, q) - if err != nil { - k.Logger(ctx).Error(fmt.Sprintf("[ICQ Resp] error in ICQ callback, error: %s, msg: %s, result: %v, type: %s, params: %v", err.Error(), msg.QueryId, msg.Result, q.QueryType, q.Request)) - return err - } - } else { - k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] callback not found for module (%s)", moduleName)) + // Once the callback is found, invoke the function + if moduleCallbackHandler.HasICQCallback(query.CallbackId) { + return moduleCallbackHandler.CallICQCallback(ctx, query.CallbackId, msg.Result, query) } } - return nil -} - -// verify the query has not exceeded its ttl -func (k Keeper) HasQueryExceededTtl(ctx sdk.Context, msg *types.MsgSubmitQueryResponse, query types.Query) (bool, error) { - k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] query %s with ttl: %d, resp time: %d.", msg.QueryId, query.Ttl, ctx.BlockHeader().Time.UnixNano())) - currBlockTime, err := cast.ToUint64E(ctx.BlockTime().UnixNano()) - if err != nil { - return false, err - } - if query.Ttl < currBlockTime { - errMsg := fmt.Sprintf("[ICQ Resp] aborting query callback due to ttl expiry! ttl is %d, time now %d for query of type %s with id %s, on chain %s", - query.Ttl, ctx.BlockTime().UnixNano(), query.QueryType, query.ChainId, msg.QueryId) - fmt.Println(errMsg) - k.Logger(ctx).Error(errMsg) - return true, nil - } - return false, nil + // If no callback was found, return an error + return types.ErrICQCallbackNotFound } +// Handle ICQ query responses by validating the proof, and calling the query's corresponding callback func (k msgServer) SubmitQueryResponse(goCtx context.Context, msg *types.MsgSubmitQueryResponse) (*types.MsgSubmitQueryResponseResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) // check if the response has an associated query stored on stride - q, found := k.GetQuery(ctx, msg.QueryId) + query, found := k.GetQuery(ctx, msg.QueryId) if !found { - k.Logger(ctx).Info("[ICQ Resp] ignoring non-existent query response (note: duplicate responses are nonexistent)") + k.Logger(ctx).Info("ICQ RESPONSE | Ignoring non-existent query response (note: duplicate responses are nonexistent)") return &types.MsgSubmitQueryResponseResponse{}, nil // technically this is an error, but will cause the entire tx to fail if we have one 'bad' message, so we can just no-op here. } @@ -161,43 +132,51 @@ func (k msgServer) SubmitQueryResponse(goCtx context.Context, msg *types.MsgSubm sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - sdk.NewAttribute(types.AttributeKeyQueryId, q.Id), + sdk.NewAttribute(types.AttributeKeyQueryId, query.Id), ), sdk.NewEvent( "query_response", sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - sdk.NewAttribute(types.AttributeKeyQueryId, q.Id), - sdk.NewAttribute(types.AttributeKeyChainId, q.ChainId), + sdk.NewAttribute(types.AttributeKeyQueryId, query.Id), + sdk.NewAttribute(types.AttributeKeyChainId, query.ChainId), ), }) - // 1. verify the response's proof, if one exists - err := k.VerifyKeyProof(ctx, msg, q) + // Verify the response's proof, if one exists + err := k.VerifyKeyProof(ctx, msg, query) if err != nil { + k.Logger(ctx).Error(utils.LogICQCallbackWithHostZone(query.ChainId, query.CallbackId, + "QUERY PROOF VERIFICATION FAILED - QueryId: %s, Error: %s", query.Id, err.Error())) return nil, err } - // 2. immediately delete the query so it cannot process again - k.DeleteQuery(ctx, q.Id) - // 3. verify the query's ttl is unexpired - ttlExceeded, err := k.HasQueryExceededTtl(ctx, msg, q) + // Immediately delete the query so it cannot process again + k.DeleteQuery(ctx, query.Id) + + // Verify the query hasn't expired (if the block time is greater than the TTL timestamp, the query is expired) + currBlockTime, err := cast.ToUint64E(ctx.BlockTime().UnixNano()) if err != nil { return nil, err } - if ttlExceeded { - k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] %s's ttl exceeded: %d < %d.", msg.QueryId, q.Ttl, ctx.BlockHeader().Time.UnixNano())) + if query.Ttl < currBlockTime { + k.Logger(ctx).Error(utils.LogICQCallbackWithHostZone(query.ChainId, query.CallbackId, + "QUERY TIMEOUT - QueryId: %s, TTL: %d, BlockTime: %d", query.Id, query.Ttl, ctx.BlockHeader().Time.UnixNano())) return &types.MsgSubmitQueryResponseResponse{}, nil } - // 4. if the query is contentless, end + // If the query is contentless, end if len(msg.Result) == 0 { - k.Logger(ctx).Info(fmt.Sprintf("[ICQ Resp] query %s is contentless, removing from store.", msg.QueryId)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, query.CallbackId, + "Query response is contentless - QueryId: %s", query.Id)) return &types.MsgSubmitQueryResponseResponse{}, nil } - // 5. call the query's associated callback function - err = k.InvokeCallback(ctx, msg, q) + // Call the query's associated callback function + err = k.InvokeCallback(ctx, msg, query) if err != nil { + k.Logger(ctx).Error(utils.LogICQCallbackWithHostZone(query.ChainId, query.CallbackId, + "Error invoking ICQ callback, error: %s, QueryId: %s, QueryType: %s, ConnectionId: %s, QueryRequest: %v, QueryReponse: %v", + err.Error(), msg.QueryId, query.QueryType, query.ConnectionId, query.Request, msg.Result)) return nil, err } diff --git a/x/interchainquery/keeper/msg_submit_query_response_test.go b/x/interchainquery/keeper/msg_submit_query_response_test.go index 2a5b3dbe7a..4ebe7ab91d 100644 --- a/x/interchainquery/keeper/msg_submit_query_response_test.go +++ b/x/interchainquery/keeper/msg_submit_query_response_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "context" + "strings" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/bech32" @@ -69,7 +70,7 @@ func (s *KeeperTestSuite) TestMsgSubmitQueryResponse_WrongProof() { s.App.InterchainqueryKeeper.SetQuery(s.Ctx, tc.query) resp, err := s.GetMsgServer().SubmitQueryResponse(tc.goCtx, &tc.validMsg) - s.Require().ErrorContains(err, "unable to verify membership proof: proof cannot be empty") + s.Require().ErrorContains(err, "Unable to verify membership proof: proof cannot be empty") s.Require().Nil(resp) } @@ -92,25 +93,20 @@ func (s *KeeperTestSuite) TestMsgSubmitQueryResponse_UnknownId() { func (s *KeeperTestSuite) TestMsgSubmitQueryResponse_ExceededTtl() { tc := s.SetupMsgSubmitQueryResponse() - tc.query.Ttl = uint64(1) // set ttl to be expired - s.App.InterchainqueryKeeper.SetQuery(s.Ctx, tc.query) - exceeded, err := s.App.InterchainqueryKeeper.HasQueryExceededTtl(s.Ctx, &tc.validMsg, tc.query) - s.Require().NoError(err) - s.Require().True(exceeded) -} - -func (s *KeeperTestSuite) TestMsgSubmitQueryResponse_NotExceededTtl() { - tc := s.SetupMsgSubmitQueryResponse() + // Remove key from the query type so to bypass the VerifyKeyProof function + tc.query.QueryType = strings.ReplaceAll(tc.query.QueryType, "key", "") - tc.query.Ttl = uint64(2545450064) * uint64(1000000000) // for test clarity, re-set ttl to August 2050, mult by nano conversion factor + // set ttl to be expired + tc.query.Ttl = uint64(1) s.App.InterchainqueryKeeper.SetQuery(s.Ctx, tc.query) - exceeded, err := s.App.InterchainqueryKeeper.HasQueryExceededTtl(s.Ctx, &tc.validMsg, tc.query) + + resp, err := s.GetMsgServer().SubmitQueryResponse(tc.goCtx, &tc.validMsg) s.Require().NoError(err) - s.Require().False(exceeded) + s.Require().NotNil(resp) - // check that the query is not in the store anymore, as it should be deleted + // check that the query was deleted (since the query timed out) _, found := s.App.InterchainqueryKeeper.GetQuery(s.Ctx, tc.query.Id) - s.Require().True(found) + s.Require().False(found) } func (s *KeeperTestSuite) TestMsgSubmitQueryResponse_FindAndInvokeCallback_WrongHostZone() { diff --git a/x/interchainquery/types/error.go b/x/interchainquery/types/error.go index 819d377651..7eb9d884fb 100644 --- a/x/interchainquery/types/error.go +++ b/x/interchainquery/types/error.go @@ -3,7 +3,8 @@ package types import "errors" var ( - ErrAlreadyFulfilled = errors.New("query already fulfilled") - ErrSucceededNoDelete = errors.New("query succeeded; do not not execute default behavior") - ErrInvalidICQProof = errors.New("icq query response failed") + ErrAlreadyFulfilled = errors.New("query already fulfilled") + ErrSucceededNoDelete = errors.New("query succeeded; do not not execute default behavior") + ErrInvalidICQProof = errors.New("icq query response failed") + ErrICQCallbackNotFound = errors.New("icq callback id not found") ) diff --git a/x/stakeibc/keeper/hooks.go b/x/stakeibc/keeper/hooks.go index eedc80161f..8f72ad12d6 100644 --- a/x/stakeibc/keeper/hooks.go +++ b/x/stakeibc/keeper/hooks.go @@ -209,8 +209,8 @@ func (k Keeper) ReinvestRewards(ctx sdk.Context) { for _, hostZone := range k.GetAllHostZone(ctx) { // only process host zones once withdrawal accounts are registered - withdrawalIca := hostZone.WithdrawalAccount - if withdrawalIca == nil { + withdrawalAccount := hostZone.WithdrawalAccount + if withdrawalAccount == nil || withdrawalAccount.Address == "" { k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Withdrawal account not registered for host zone")) continue } diff --git a/x/stakeibc/keeper/icqcallbacks_delegator_shares.go b/x/stakeibc/keeper/icqcallbacks_delegator_shares.go index 034bf6fe09..dca3f72e94 100644 --- a/x/stakeibc/keeper/icqcallbacks_delegator_shares.go +++ b/x/stakeibc/keeper/icqcallbacks_delegator_shares.go @@ -1,14 +1,13 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/spf13/cast" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/Stride-Labs/stride/v4/utils" epochtypes "github.com/Stride-Labs/stride/v4/x/epochs/types" icqtypes "github.com/Stride-Labs/stride/v4/x/interchainquery/types" "github.com/Stride-Labs/stride/v4/x/stakeibc/types" @@ -26,70 +25,63 @@ import ( // // Note: for now, to get proofs in your ICQs, you need to query the entire store on the host zone! e.g. "store/bank/key" func DelegatorSharesCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error { - hostZone, found := k.GetHostZone(ctx, query.GetChainId()) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, ICQCallbackID_Delegation, + "Starting delegator shares callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId)) + + // Confirm host exists + chainId := query.ChainId + hostZone, found := k.GetHostZone(ctx, chainId) if !found { - errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg) + return sdkerrors.Wrapf(types.ErrHostZoneNotFound, "no registered zone for queried chain ID (%s)", chainId) } // Unmarshal the query response which returns a delegation object for the delegator/validator pair queriedDelgation := stakingtypes.Delegation{} err := k.cdc.Unmarshal(args, &queriedDelgation) if err != nil { - errMsg := fmt.Sprintf("unable to unmarshal queried delegation info for zone %s, err: %s", hostZone.ChainId, err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg) + return sdkerrors.Wrapf(types.ErrMarshalFailure, "unable to unmarshal query response into Delegation type, err: %s", err.Error()) } - k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: HostZone: %s, Delegator: %s, Validator: %s, Shares: %v", - hostZone.ChainId, queriedDelgation.DelegatorAddress, queriedDelgation.ValidatorAddress, queriedDelgation.Shares)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Delegation, "Query response - Delegator: %s, Validator: %s, Shares: %v", + queriedDelgation.DelegatorAddress, queriedDelgation.ValidatorAddress, queriedDelgation.Shares)) - // ensure ICQ can be issued now! else fail the callback - isWithinWindow, err := k.IsWithinBufferWindow(ctx) + // Ensure ICQ can be issued now, else fail the callback + withinBufferWindow, err := k.IsWithinBufferWindow(ctx) if err != nil { - errMsg := fmt.Sprintf("unable to determine if ICQ callback is inside buffer window, err: %s", err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, errMsg) - } else if !isWithinWindow { - k.Logger(ctx).Error("delegator shares callback is outside ICQ window") - return nil + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "unable to determine if ICQ callback is inside buffer window, err: %s", err.Error()) + } + if !withinBufferWindow { + return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, "callback is outside ICQ window") } - // Grab the validator object form the hostZone using the address returned from the query + // Grab the validator object from the hostZone using the address returned from the query validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedDelgation.ValidatorAddress) if !found { - errMsg := fmt.Sprintf("no registered validator for address (%s)", queriedDelgation.ValidatorAddress) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrValidatorNotFound, errMsg) + return sdkerrors.Wrapf(types.ErrValidatorNotFound, "no registered validator for address (%s)", queriedDelgation.ValidatorAddress) } - // get the validator's internal exchange rate, aborting if it hasn't been updated this epoch + // Get the validator's internal exchange rate, aborting if it hasn't been updated this epoch strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH) if !found { - k.Logger(ctx).Error("failed to find stride epoch") - return sdkerrors.Wrapf(sdkerrors.ErrNotFound, "no epoch number for epoch (%s)", epochtypes.STRIDE_EPOCH) + return sdkerrors.Wrapf(sdkerrors.ErrNotFound, "unable to get epoch tracker for epoch (%s)", epochtypes.STRIDE_EPOCH) } - if validator.InternalExchangeRate.EpochNumber != strideEpochTracker.GetEpochNumber() { - errMsg := fmt.Sprintf("DelegationCallback: validator (%s) internal exchange rate has not been updated this epoch (epoch #%d)", - validator.Address, strideEpochTracker.GetEpochNumber()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg) + if validator.InternalExchangeRate.EpochNumber != strideEpochTracker.EpochNumber { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, + "validator (%s) internal exchange rate has not been updated this epoch (epoch #%d)", validator.Address, strideEpochTracker.EpochNumber) } - // TODO: make sure conversion math precision matches the sdk's staking module's version (we did it slightly differently) + // Calculate the number of tokens delegated (using the internal exchange rate) // note: truncateInt per https://github.com/cosmos/cosmos-sdk/blob/cb31043d35bad90c4daa923bb109f38fd092feda/x/staking/types/validator.go#L431 - validatorTokens := queriedDelgation.Shares.Mul(validator.InternalExchangeRate.InternalTokensToSharesRate).TruncateInt() - k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: HostZone: %s, Validator: %s, Previous NumTokens: %v, Current NumTokens: %v", - hostZone.ChainId, validator.Address, validator.DelegationAmt, validatorTokens)) + delegatedTokens := queriedDelgation.Shares.Mul(validator.InternalExchangeRate.InternalTokensToSharesRate).TruncateInt() + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Delegation, + "Previous Delegation: %v, Current Delegation: %v", validator.DelegationAmt, delegatedTokens)) // Confirm the validator has actually been slashed - if validatorTokens.Equal(validator.DelegationAmt) { - k.Logger(ctx).Info(fmt.Sprintf("DelegationCallback: Validator (%s) was not slashed", validator.Address)) + if delegatedTokens.Equal(validator.DelegationAmt) { + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Delegation, "Validator was not slashed")) return nil - } else if validatorTokens.GT(validator.DelegationAmt) { - errMsg := fmt.Sprintf("DelegationCallback: Validator (%s) tokens returned from query is greater than the DelegationAmt", validator.Address) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg) + } + if delegatedTokens.GT(validator.DelegationAmt) { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "Validator (%s) tokens returned from query is greater than the DelegationAmt", validator.Address) } // TODO(TESTS-171) add some safety checks here (e.g. we could query the slashing module to confirm the decr in tokens was due to slash) @@ -97,37 +89,40 @@ func DelegatorSharesCallback(k Keeper, ctx sdk.Context, args []byte, query icqty // NOTE: we assume any decrease in delegation amt that's not tracked via records is a slash // Get slash percentage - slashAmount := validator.DelegationAmt.Sub(validatorTokens) + slashAmount := validator.DelegationAmt.Sub(delegatedTokens) + slashPct := sdk.NewDecFromInt(slashAmount).Quo(sdk.NewDecFromInt(validator.DelegationAmt)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Delegation, + "Validator was slashed! Validator: %s, Delegator: %s, Delegation in State: %v, Delegation from ICQ %v, Slash Amount: %v, Slash Pct: %v", + validator.Address, queriedDelgation.DelegatorAddress, validator.DelegationAmt, delegatedTokens, slashAmount, slashPct)) - weight, err := cast.ToInt64E(validator.Weight) + // Abort if the slash was greater than the safety threshold + slashThreshold, err := cast.ToInt64E(k.GetParam(ctx, types.KeySafetyMaxSlashPercent)) if err != nil { - errMsg := fmt.Sprintf("unable to convert validator weight to int64, err: %s", err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrIntCast, errMsg) + return err + } + slashThresholdDecimal := sdk.NewDec(slashThreshold).Quo(sdk.NewDec(100)) + if slashPct.GT(slashThresholdDecimal) { + return sdkerrors.Wrapf(types.ErrSlashExceedsSafetyThreshold, + "Validator slashed but ABORTING update, slash (%v) is greater than safety threshold (%v)", slashPct, slashThresholdDecimal) } - slashPct := sdk.NewDecFromInt(slashAmount).Quo(sdk.NewDecFromInt(validator.DelegationAmt)) - k.Logger(ctx).Info(fmt.Sprintf("ICQ'd Delegation Amount Mismatch, HostZone: %s, Validator: %s, Delegator: %s, Records Tokens: %v, Tokens from ICQ %v, Slash Amount: %v, Slash Pct: %v!", - hostZone.ChainId, validator.Address, queriedDelgation.DelegatorAddress, validator.DelegationAmt, validatorTokens, slashAmount, slashPct)) - - // Abort if the slash was greater than 10% - tenPercent := sdk.NewDec(10).Quo(sdk.NewDec(100)) - if slashPct.GT(tenPercent) { - errMsg := fmt.Sprintf("DelegationCallback: Validator (%s) slashed but ABORTING update, slash is greater than 0.10 (%d)", validator.Address, slashPct) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrSlashGtTenPct, errMsg) + // Update the validator weight and delegation reflect to reflect the slash + weight, err := cast.ToInt64E(validator.Weight) + if err != nil { + return sdkerrors.Wrapf(types.ErrIntCast, "unable to convert validator weight to int64, err: %s", err.Error()) } + weightAdjustment := sdk.NewDecFromInt(delegatedTokens).Quo(sdk.NewDecFromInt(validator.DelegationAmt)) - // Update the host zone and validator to reflect the weight and delegation change - weightAdjustment := sdk.NewDecFromInt(validatorTokens).Quo(sdk.NewDecFromInt(validator.DelegationAmt)) - validator.Weight = sdk.NewDec(int64(weight)).Mul(weightAdjustment).TruncateInt().Uint64() + validator.Weight = sdk.NewDec(weight).Mul(weightAdjustment).TruncateInt().Uint64() validator.DelegationAmt = validator.DelegationAmt.Sub(slashAmount) + // Update the validator on the host zone hostZone.StakedBal = hostZone.StakedBal.Sub(slashAmount) hostZone.Validators[valIndex] = &validator k.SetHostZone(ctx, hostZone) - k.Logger(ctx).Info(fmt.Sprintf("Validator (%s) slashed! Delegation updated to: %v", validator.Address, validator.DelegationAmt)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Delegation, + "Delegation updated to: %v, Weight updated to: %v", validator.DelegationAmt, validator.Weight)) return nil } diff --git a/x/stakeibc/keeper/icqcallbacks_delegator_shares_test.go b/x/stakeibc/keeper/icqcallbacks_delegator_shares_test.go index 88f1f0f029..e14de73d03 100644 --- a/x/stakeibc/keeper/icqcallbacks_delegator_shares_test.go +++ b/x/stakeibc/keeper/icqcallbacks_delegator_shares_test.go @@ -176,7 +176,7 @@ func (s *KeeperTestSuite) TestDelegatorSharesCallback_InvalidCallbackArgs() { invalidArgs := []byte("random bytes") err := stakeibckeeper.DelegatorSharesCallback(s.App.StakeibcKeeper, s.Ctx, invalidArgs, tc.validArgs.query) - expectedErrMsg := "unable to unmarshal queried delegation info for zone GAIA, " + expectedErrMsg := "unable to unmarshal query response into Delegation type, " expectedErrMsg += "err: unexpected EOF: unable to marshal data structure" s.Require().EqualError(err, expectedErrMsg) } @@ -209,7 +209,7 @@ func (s *KeeperTestSuite) TestDelegatorSharesCallback_OutsideBufferWindow() { // In this case, we should return success instead of error, but we should exit early before updating the validator's state err := stakeibckeeper.DelegatorSharesCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - s.Require().NoError(err, "delegator shares callback callback error") + s.Require().ErrorContains(err, "callback is outside ICQ window") s.checkStateIfValidatorNotSlashed(tc) } @@ -232,7 +232,7 @@ func (s *KeeperTestSuite) TestDelegatorSharesCallback_ExchangeRateNotFound() { s.App.StakeibcKeeper.SetEpochTracker(s.Ctx, epochTracker) err := stakeibckeeper.DelegatorSharesCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - s.Require().EqualError(err, "DelegationCallback: validator (valoper2) internal exchange rate has not been updated this epoch (epoch #2): invalid request") + s.Require().EqualError(err, "validator (valoper2) internal exchange rate has not been updated this epoch (epoch #2): invalid request") } func (s *KeeperTestSuite) TestDelegatorSharesCallback_NoSlashOccurred() { @@ -263,7 +263,7 @@ func (s *KeeperTestSuite) TestDelegatorSharesCallback_InvalidNumTokens() { badCallbackArgs := s.CreateDelegatorSharesQueryResponse(valAddress, numShares) err := stakeibckeeper.DelegatorSharesCallback(s.App.StakeibcKeeper, s.Ctx, badCallbackArgs, tc.validArgs.query) - expectedErrMsg := "DelegationCallback: Validator (valoper2) tokens returned from query is greater than the DelegationAmt: invalid request" + expectedErrMsg := "Validator (valoper2) tokens returned from query is greater than the DelegationAmt: invalid request" s.Require().EqualError(err, expectedErrMsg) } @@ -291,7 +291,7 @@ func (s *KeeperTestSuite) TestDelegatorSharesCallback_SlashGtTenPercent() { badCallbackArgs := s.CreateDelegatorSharesQueryResponse(valAddress, sdk.NewDec(1600)) err := stakeibckeeper.DelegatorSharesCallback(s.App.StakeibcKeeper, s.Ctx, badCallbackArgs, tc.validArgs.query) - expectedErrMsg := "DelegationCallback: Validator (valoper2) slashed but ABORTING update, " - expectedErrMsg += "slash is greater than 0.10 (0.200000000000000000): slash is greater than 10 percent" + expectedErrMsg := "Validator slashed but ABORTING update, slash (0.200000000000000000) is greater than safety threshold (0.100000000000000000): " + expectedErrMsg += "slash is greater than safety threshold" s.Require().EqualError(err, expectedErrMsg) } diff --git a/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate.go b/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate.go index 722f682bdc..c7d3211f30 100644 --- a/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate.go +++ b/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate.go @@ -1,13 +1,12 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/Stride-Labs/stride/v4/utils" epochtypes "github.com/Stride-Labs/stride/v4/x/epochs/types" icqtypes "github.com/Stride-Labs/stride/v4/x/interchainquery/types" "github.com/Stride-Labs/stride/v4/x/stakeibc/types" @@ -23,41 +22,41 @@ import ( // // This is the callback from query #1 func ValidatorExchangeRateCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error { - hostZone, found := k.GetHostZone(ctx, query.GetChainId()) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, ICQCallbackID_Validator, + "Starting validator exchange rate balance callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId)) + + // Confirm host exists + chainId := query.ChainId + hostZone, found := k.GetHostZone(ctx, query.ChainId) if !found { - errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg) + return sdkerrors.Wrapf(types.ErrHostZoneNotFound, "no registered zone for queried chain ID (%s)", chainId) } + + // Unmarshal the query response args into a Validator struct queriedValidator := stakingtypes.Validator{} err := k.cdc.Unmarshal(args, &queriedValidator) if err != nil { - errMsg := fmt.Sprintf("unable to unmarshal queriedValidator info for zone %s, err: %s", hostZone.ChainId, err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg) + return sdkerrors.Wrapf(types.ErrMarshalFailure, "unable to unmarshal query response into Validator type, err: %s", err.Error()) } - k.Logger(ctx).Info(fmt.Sprintf("ValidatorCallback: HostZone %s, Queried Validator %v, Jailed: %v, Tokens: %v, Shares: %v", - hostZone.ChainId, queriedValidator.OperatorAddress, queriedValidator.Jailed, queriedValidator.Tokens, queriedValidator.DelegatorShares)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Validator, "Query response - Validator: %s, Jailed: %v, Tokens: %v, Shares: %v", + queriedValidator.OperatorAddress, queriedValidator.Jailed, queriedValidator.Tokens, queriedValidator.DelegatorShares)) - // ensure ICQ can be issued now! else fail the callback + // Ensure ICQ can be issued now, else fail the callback withinBufferWindow, err := k.IsWithinBufferWindow(ctx) if err != nil { - errMsg := fmt.Sprintf("unable to determine if ICQ callback is inside buffer window, err: %s", err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, errMsg) - } else if !withinBufferWindow { - k.Logger(ctx).Error("validator exchange rate callback is outside ICQ window") - return nil + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "unable to determine if ICQ callback is inside buffer window, err: %s", err.Error()) + } + if !withinBufferWindow { + return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, "callback is outside ICQ window") } - // get the validator from the host zone + // Get the validator from the host zone validator, valIndex, found := GetValidatorFromAddress(hostZone.Validators, queriedValidator.OperatorAddress) if !found { - errMsg := fmt.Sprintf("no registered validator for address (%s)", queriedValidator.OperatorAddress) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrValidatorNotFound, errMsg) + return sdkerrors.Wrapf(types.ErrValidatorNotFound, "no registered validator for address (%s)", queriedValidator.OperatorAddress) } - // get the stride epoch number + + // Get the stride epoch number strideEpochTracker, found := k.GetEpochTracker(ctx, epochtypes.STRIDE_EPOCH) if !found { k.Logger(ctx).Error("failed to find stride epoch") @@ -67,9 +66,8 @@ func ValidatorExchangeRateCallback(k Keeper, ctx sdk.Context, args []byte, query // If the validator's delegation shares is 0, we'll get a division by zero error when trying to get the exchange rate // because `validator.TokensFromShares` uses delegation shares in the denominator if queriedValidator.DelegatorShares.IsZero() { - errMsg := fmt.Sprintf("can't calculate validator internal exchange rate because delegation amount is 0 (validator: %s)", validator.Address) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrDivisionByZero, errMsg) + return sdkerrors.Wrapf(types.ErrDivisionByZero, + "can't calculate validator internal exchange rate because delegation amount is 0 (validator: %s)", validator.Address) } // We want the validator's internal exchange rate which is held internally behind `validator.TokensFromShares` @@ -84,15 +82,13 @@ func ValidatorExchangeRateCallback(k Keeper, ctx sdk.Context, args []byte, query hostZone.Validators[valIndex] = &validator k.SetHostZone(ctx, hostZone) - k.Logger(ctx).Info(fmt.Sprintf("ValidatorCallback: HostZone %s, Validator %v, tokensFromShares %v", - hostZone.ChainId, validator.Address, validator.InternalExchangeRate.InternalTokensToSharesRate)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_Validator, "Validator Internal Exchange Rate: %v", + validator.InternalExchangeRate.InternalTokensToSharesRate)) - // armed with the exch rate, we can now query the (val,del) delegation - err = k.QueryDelegationsIcq(ctx, hostZone, queriedValidator.OperatorAddress) - if err != nil { - errMsg := fmt.Sprintf("ValidatorCallback: failed to query delegation, zone %s, err: %s", hostZone.ChainId, err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrICQFailed, errMsg) + // Armed with the exch rate, we can now query the (validator,delegator) delegation + if err := k.QueryDelegationsIcq(ctx, hostZone, queriedValidator.OperatorAddress); err != nil { + return sdkerrors.Wrapf(types.ErrICQFailed, "Failed to query delegations, err: %s", err.Error()) } + return nil } diff --git a/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate_test.go b/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate_test.go index bea5977e3e..c1108ee880 100644 --- a/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate_test.go +++ b/x/stakeibc/keeper/icqcallbacks_validator_exchange_rate_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + "fmt" + sdkmath "cosmossdk.io/math" ibctesting "github.com/cosmos/ibc-go/v5/testing" @@ -27,6 +29,7 @@ type ValidatorICQCallbackTestCase struct { initialState ValidatorICQCallbackState validArgs ValidatorICQCallbackArgs valIndexQueried int + valIndexInvalid int expectedExchangeRate sdk.Dec } @@ -44,8 +47,10 @@ func (s *KeeperTestSuite) SetupValidatorICQCallback() ValidatorICQCallbackTestCa // We don't actually need a transfer channel for this test, but we do need to have IBC support for timeouts s.CreateTransferChannel(HostChainId) - valAddress := "valoper1" - valIndexQueried := 0 // index in the validators array + // These must be valid addresses, otherwise the bech decoding will fail + valAddress := "cosmosvaloper1uk4ze0x4nvh4fk0xm4jdud58eqn4yxhrdt795p" + delegatorAddress := "cosmos1sy63lffevueudvvlvh2lf6s387xh9xq72n3fsy6n2gr5hm6u2szs2v0ujm" + // In this example, the validator has 2000 shares, originally had 2000 tokens, // and now has 1000 tokens (after being slashed) initialExchangeRate := sdk.NewDec(1) @@ -58,7 +63,7 @@ func (s *KeeperTestSuite) SetupValidatorICQCallback() ValidatorICQCallbackTestCa ChainId: HostChainId, ConnectionId: ibctesting.FirstConnectionID, DelegationAccount: &stakeibctypes.ICAAccount{ - Address: "cosmos_DELEGATION", + Address: delegatorAddress, Target: stakeibctypes.ICAAccountType_DELEGATION, }, Validators: []*stakeibctypes.Validator{ @@ -73,11 +78,15 @@ func (s *KeeperTestSuite) SetupValidatorICQCallback() ValidatorICQCallbackTestCa // This validator isn't being queried { Name: "val2", - Address: "valoper2", + Address: "cosmos_invalid_address", }, }, } + // index in the validators array + valIndexQueried := 0 + valIndexInvalid := 1 + // This will make the current time 90% through the epoch strideEpochTracker := stakeibctypes.EpochTracker{ EpochIdentifier: epochtypes.STRIDE_EPOCH, @@ -103,6 +112,7 @@ func (s *KeeperTestSuite) SetupValidatorICQCallback() ValidatorICQCallbackTestCa callbackArgs: queryResponse, }, valIndexQueried: valIndexQueried, + valIndexInvalid: valIndexInvalid, expectedExchangeRate: expectedExchangeRate, } } @@ -138,7 +148,7 @@ func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_InvalidCallbackArgs( invalidArgs := []byte("random bytes") err := stakeibckeeper.ValidatorExchangeRateCallback(s.App.StakeibcKeeper, s.Ctx, invalidArgs, tc.validArgs.query) - expectedErrMsg := "unable to unmarshal queriedValidator info for zone GAIA, " + expectedErrMsg := "unable to unmarshal query response into Validator type, " expectedErrMsg += "err: unexpected EOF: unable to marshal data structure" s.Require().EqualError(err, expectedErrMsg) } @@ -168,9 +178,8 @@ func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_OutsideBufferWindow( s.App.StakeibcKeeper.SetEpochTracker(s.Ctx, epochTracker) - // In this case, we should return success instead of error, but we should exit early before updating the validator's exchange rate err := stakeibckeeper.ValidatorExchangeRateCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - s.Require().NoError(err, "validator exchange rate callback error") + s.Require().ErrorContains(err, "callback is outside ICQ window") // Confirm validator's exchange rate did not update hostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, tc.initialState.hostZone.ChainId) @@ -185,11 +194,21 @@ func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_ValidatorNotFound() tc := s.SetupValidatorICQCallback() // Update the callback args to contain a validator address that doesn't exist - badCallbackArgs := s.CreateValidatorQueryResponse("fake_val", 0, 0) + badCallbackArgs := s.CreateValidatorQueryResponse("fake_val", 1, 1) err := stakeibckeeper.ValidatorExchangeRateCallback(s.App.StakeibcKeeper, s.Ctx, badCallbackArgs, tc.validArgs.query) s.Require().EqualError(err, "no registered validator for address (fake_val): validator not found") } +func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_InvalidValidatorAddress() { + tc := s.SetupValidatorICQCallback() + + // Update callback arsg to contain a validator address that does exist, but is invalid + invalidAddress := tc.initialState.hostZone.Validators[tc.valIndexInvalid].Address + badCallbackArgs := s.CreateValidatorQueryResponse(invalidAddress, 1, 1) + err := stakeibckeeper.ValidatorExchangeRateCallback(s.App.StakeibcKeeper, s.Ctx, badCallbackArgs, tc.validArgs.query) + s.Require().ErrorContains(err, "Failed to query delegations, err: invalid validator address, could not decode") +} + func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_DelegatorSharesZero() { tc := s.SetupValidatorICQCallback() @@ -198,7 +217,8 @@ func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_DelegatorSharesZero( badCallbackArgs := s.CreateValidatorQueryResponse(valAddress, 1000, 0) // the 1000 is arbitrary, the zero here is what matters err := stakeibckeeper.ValidatorExchangeRateCallback(s.App.StakeibcKeeper, s.Ctx, badCallbackArgs, tc.validArgs.query) - expectedErrMsg := "can't calculate validator internal exchange rate because delegation amount is 0 (validator: valoper1): division by zero" + expectedErrMsg := "can't calculate validator internal exchange rate because delegation amount is 0 " + expectedErrMsg += fmt.Sprintf("(validator: %s): division by zero", valAddress) s.Require().EqualError(err, expectedErrMsg) } @@ -212,7 +232,7 @@ func (s *KeeperTestSuite) TestValidatorExchangeRateCallback_DelegationQueryFaile err := stakeibckeeper.ValidatorExchangeRateCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - expectedErrMsg := "ValidatorCallback: failed to query delegation, zone GAIA, err: Zone GAIA is missing a delegation address!: " - expectedErrMsg += "ICA acccount not found on host zone: failed to submit ICQ" - s.Require().EqualError(err, expectedErrMsg) + expectedErr := "Failed to query delegations, err: no delegation address found for GAIA: " + expectedErr += "ICA acccount not found on host zone: failed to submit ICQ" + s.Require().EqualError(err, expectedErr) } diff --git a/x/stakeibc/keeper/icqcallbacks_withdrawal_balance.go b/x/stakeibc/keeper/icqcallbacks_withdrawal_balance.go index d9735e1bad..1c0d6a69f0 100644 --- a/x/stakeibc/keeper/icqcallbacks_withdrawal_balance.go +++ b/x/stakeibc/keeper/icqcallbacks_withdrawal_balance.go @@ -9,71 +9,67 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/spf13/cast" + "github.com/Stride-Labs/stride/v4/utils" icqtypes "github.com/Stride-Labs/stride/v4/x/interchainquery/types" "github.com/Stride-Labs/stride/v4/x/stakeibc/types" ) // WithdrawalBalanceCallback is a callback handler for WithdrawalBalance queries. +// The query response will return the withdrawal account balance +// If the balance is non-zero, ICA MsgSends are submitted to transfer from the withdrawal account +// to the delegation account (for reinvestment) and fee account (for commission) // Note: for now, to get proofs in your ICQs, you need to query the entire store on the host zone! e.g. "store/bank/key" func WithdrawalBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query icqtypes.Query) error { - k.Logger(ctx).Info(fmt.Sprintf("WithdrawalBalanceCallback executing, QueryId: %vs, Host: %s, QueryType: %s, Connection: %s", - query.Id, query.ChainId, query.QueryType, query.ConnectionId)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(query.ChainId, ICQCallbackID_WithdrawalBalance, + "Starting withdrawal balance callback, QueryId: %vs, QueryType: %s, Connection: %s", query.Id, query.QueryType, query.ConnectionId)) - hostZone, found := k.GetHostZone(ctx, query.GetChainId()) + // Confirm host exists + chainId := query.ChainId + hostZone, found := k.GetHostZone(ctx, chainId) if !found { - errMsg := fmt.Sprintf("no registered zone for queried chain ID (%s)", query.GetChainId()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrHostZoneNotFound, errMsg) + return sdkerrors.Wrapf(types.ErrHostZoneNotFound, "no registered zone for queried chain ID (%s)", chainId) } - // Unmarshal the CB args into a coin type + // Unmarshal the query response args into a coin type withdrawalBalanceCoin := sdk.Coin{} err := k.cdc.Unmarshal(args, &withdrawalBalanceCoin) if err != nil { - errMsg := fmt.Sprintf("unable to unmarshal balance in callback args for zone: %s, err: %s", hostZone.ChainId, err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrMarshalFailure, errMsg) + return sdkerrors.Wrapf(types.ErrUnmarshalFailure, "unable to unmarshal query response into Coin type, err: %s", err.Error()) } + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance, "Query response - Coin: %+v", withdrawalBalanceCoin)) // Check if the coin is nil (which would indicate the account never had a balance) if withdrawalBalanceCoin.IsNil() || withdrawalBalanceCoin.Amount.IsNil() { - k.Logger(ctx).Info(fmt.Sprintf("WithdrawalBalanceCallback: balance query returned a nil coin for address %s on %s, meaning the account has never had a balance on the host", - hostZone.WithdrawalAccount.GetAddress(), hostZone.ChainId)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance, + "Balance query returned a nil coin for address %v, meaning the account has never had a balance on the host", + hostZone.WithdrawalAccount.GetAddress())) return nil } // Confirm the balance is greater than zero if withdrawalBalanceCoin.Amount.LTE(sdkmath.ZeroInt()) { - k.Logger(ctx).Info(fmt.Sprintf("WithdrawalBalanceCallback: no balance to transfer for zone: %s, accAddr: %v, coin: %v", - hostZone.ChainId, hostZone.WithdrawalAccount.GetAddress(), withdrawalBalanceCoin.String())) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance, + "No balance to transfer for address: %v, coin: %v", hostZone.WithdrawalAccount.GetAddress(), withdrawalBalanceCoin.String())) return nil } - // Sweep the withdrawal account balance, to the commission and the delegation accounts - k.Logger(ctx).Info(fmt.Sprintf("ICA Bank Sending %v%s from withdrawalAddr to delegationAddr.", - withdrawalBalanceCoin.Amount, withdrawalBalanceCoin.Denom)) - - withdrawalAccount := hostZone.GetWithdrawalAccount() - if withdrawalAccount == nil { - errMsg := fmt.Sprintf("WithdrawalBalanceCallback: no withdrawal account found for zone: %s", hostZone.ChainId) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrICAAccountNotFound, errMsg) + // Get the host zone's ICA accounts + withdrawalAccount := hostZone.WithdrawalAccount + if withdrawalAccount == nil || withdrawalAccount.Address == "" { + return sdkerrors.Wrapf(types.ErrICAAccountNotFound, "no withdrawal account found for %s", chainId) } - delegationAccount := hostZone.GetDelegationAccount() - if delegationAccount == nil { - errMsg := fmt.Sprintf("WithdrawalBalanceCallback: no delegation account found for zone: %s", hostZone.ChainId) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrICAAccountNotFound, errMsg) + delegationAccount := hostZone.DelegationAccount + if delegationAccount == nil || delegationAccount.Address == "" { + return sdkerrors.Wrapf(types.ErrICAAccountNotFound, "no delegation account found for %s", chainId) } - feeAccount := hostZone.GetFeeAccount() - if feeAccount == nil { - errMsg := fmt.Sprintf("WithdrawalBalanceCallback: no fee account found for zone: %s", hostZone.ChainId) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrICAAccountNotFound, errMsg) + feeAccount := hostZone.FeeAccount + if feeAccount == nil || feeAccount.Address == "" { + return sdkerrors.Wrapf(types.ErrICAAccountNotFound, "no fee account found for %s", chainId) } + // Determine the stride commission rate to the relevant portion can be sent to the fee account params := k.GetParams(ctx) - strideCommissionInt, err := cast.ToInt64E(params.GetStrideCommission()) + strideCommissionInt, err := cast.ToInt64E(params.StrideCommission) if err != nil { return err } @@ -81,47 +77,50 @@ func WithdrawalBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query icq // check that stride commission is between 0 and 1 strideCommission := sdk.NewDec(strideCommissionInt).Quo(sdk.NewDec(100)) if strideCommission.LT(sdk.ZeroDec()) || strideCommission.GT(sdk.OneDec()) { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Aborting reinvestment callback -- Stride commission must be between 0 and 1!") + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Aborting withdrawal balance callback -- Stride commission must be between 0 and 1!") } + // Split out the reinvestment amount from the fee amount withdrawalBalanceAmount := withdrawalBalanceCoin.Amount - strideClaim := strideCommission.Mul(sdk.NewDecFromInt(withdrawalBalanceAmount)) - strideClaimFloored := strideClaim.TruncateInt() - - // back the reinvestment amount out of the total less the commission - reinvestAmountCeil := sdkmath.NewInt(withdrawalBalanceAmount.Int64()).Sub(strideClaimFloored) + feeAmount := strideCommission.Mul(sdk.NewDecFromInt(withdrawalBalanceAmount)).TruncateInt() + reinvestAmount := withdrawalBalanceAmount.Sub(feeAmount) - // TODO(TEST-112) safety check, balances should add to original amount - if (strideClaimFloored.Int64() + reinvestAmountCeil.Int64()) != withdrawalBalanceAmount.Int64() { - ctx.Logger().Error(fmt.Sprintf("Error with withdraw logic: %v, Fee portion: %v, reinvestPortion %v", withdrawalBalanceAmount, strideClaimFloored, reinvestAmountCeil)) + // Safety check, balances should add to original amount + if !feeAmount.Add(reinvestAmount).Equal(withdrawalBalanceAmount) { + k.Logger(ctx).Error(fmt.Sprintf("Error with withdraw logic: %v, Fee Portion: %v, Reinvest Portion %v", withdrawalBalanceAmount, feeAmount, reinvestAmount)) return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Failed to subdivide rewards to feeAccount and delegationAccount") } - strideCoin := sdk.NewCoin(withdrawalBalanceCoin.Denom, strideClaimFloored) - reinvestCoin := sdk.NewCoin(withdrawalBalanceCoin.Denom, reinvestAmountCeil) + + // Prepare MsgSends from the withdrawal account + feeCoin := sdk.NewCoin(withdrawalBalanceCoin.Denom, feeAmount) + reinvestCoin := sdk.NewCoin(withdrawalBalanceCoin.Denom, reinvestAmount) var msgs []sdk.Msg - if strideCoin.Amount.Int64() > 0 { + if feeCoin.Amount.GT(sdk.ZeroInt()) { msgs = append(msgs, &banktypes.MsgSend{ - FromAddress: withdrawalAccount.GetAddress(), - ToAddress: feeAccount.GetAddress(), - Amount: sdk.NewCoins(strideCoin), + FromAddress: withdrawalAccount.Address, + ToAddress: feeAccount.Address, + Amount: sdk.NewCoins(feeCoin), }) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance, + "Preparing MsgSends of %v from the withdrawal account to the fee account (for commission)", feeCoin.String())) } - if reinvestCoin.Amount.Int64() > 0 { + if reinvestCoin.Amount.GT(sdk.ZeroInt()) { msgs = append(msgs, &banktypes.MsgSend{ - FromAddress: withdrawalAccount.GetAddress(), - ToAddress: delegationAccount.GetAddress(), + FromAddress: withdrawalAccount.Address, + ToAddress: delegationAccount.Address, Amount: sdk.NewCoins(reinvestCoin), }) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance, + "Preparing MsgSends of %v from the withdrawal account to the delegation account (for reinvestment)", reinvestCoin.String())) } - ctx.Logger().Info(fmt.Sprintf("Submitting withdrawal sweep messages for: %v", msgs)) - // add callback data + // add callback data before calling reinvestment ICA reinvestCallback := types.ReinvestCallback{ ReinvestAmount: reinvestCoin, HostZoneId: hostZone.ChainId, } - k.Logger(ctx).Info(fmt.Sprintf("Marshalling ReinvestCallback args: %v", reinvestCallback)) + k.Logger(ctx).Info(utils.LogICQCallbackWithHostZone(chainId, ICQCallbackID_WithdrawalBalance, "Marshalling ReinvestCallback args: %v", reinvestCallback)) marshalledCallbackArgs, err := k.MarshalReinvestCallbackArgs(ctx, reinvestCallback) if err != nil { return err @@ -130,9 +129,7 @@ func WithdrawalBalanceCallback(k Keeper, ctx sdk.Context, args []byte, query icq // Send the transaction through SubmitTx _, err = k.SubmitTxsStrideEpoch(ctx, hostZone.ConnectionId, msgs, *withdrawalAccount, ICACallbackID_Reinvest, marshalledCallbackArgs) if err != nil { - errMsg := fmt.Sprintf("Failed to SubmitTxs for %s - %s, Messages: %v | err: %s", hostZone.ChainId, hostZone.ConnectionId, msgs, err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrICATxFailed, errMsg) + return sdkerrors.Wrapf(types.ErrICATxFailed, "Failed to SubmitTxs, Messages: %v, err: %s", msgs, err.Error()) } ctx.EventManager().EmitEvent( diff --git a/x/stakeibc/keeper/icqcallbacks_withdrawal_balance_test.go b/x/stakeibc/keeper/icqcallbacks_withdrawal_balance_test.go index 067aceba4b..79038a41a4 100644 --- a/x/stakeibc/keeper/icqcallbacks_withdrawal_balance_test.go +++ b/x/stakeibc/keeper/icqcallbacks_withdrawal_balance_test.go @@ -199,8 +199,8 @@ func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_InvalidArgs() { invalidArgs := []byte("random bytes") err := stakeibckeeper.WithdrawalBalanceCallback(s.App.StakeibcKeeper, s.Ctx, invalidArgs, tc.validArgs.query) - expectedErrMsg := "unable to unmarshal balance in callback args for zone: GAIA, " - expectedErrMsg += "err: unexpected EOF: unable to marshal data structure" + expectedErrMsg := "unable to unmarshal query response into Coin type, " + expectedErrMsg += "err: unexpected EOF: unable to unmarshal data structure" s.Require().EqualError(err, expectedErrMsg) } @@ -213,9 +213,7 @@ func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_NoWithdrawalAccount() { s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) err := stakeibckeeper.WithdrawalBalanceCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - expectedErrMsg := "WithdrawalBalanceCallback: no withdrawal account found for zone: GAIA: " - expectedErrMsg += "ICA acccount not found on host zone" - s.Require().EqualError(err, expectedErrMsg) + s.Require().EqualError(err, "no withdrawal account found for GAIA: ICA acccount not found on host zone") } func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_NoDelegationAccount() { @@ -227,9 +225,7 @@ func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_NoDelegationAccount() { s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) err := stakeibckeeper.WithdrawalBalanceCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - expectedErrMsg := "WithdrawalBalanceCallback: no delegation account found for zone: GAIA: " - expectedErrMsg += "ICA acccount not found on host zone" - s.Require().EqualError(err, expectedErrMsg) + s.Require().EqualError(err, "no delegation account found for GAIA: ICA acccount not found on host zone") } func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_NoFeeAccount() { @@ -241,9 +237,7 @@ func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_NoFeeAccount() { s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) err := stakeibckeeper.WithdrawalBalanceCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - expectedErrMsg := "WithdrawalBalanceCallback: no fee account found for zone: GAIA: " - expectedErrMsg += "ICA acccount not found on host zone" - s.Require().EqualError(err, expectedErrMsg) + s.Require().EqualError(err, "no fee account found for GAIA: ICA acccount not found on host zone") } func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_FailedSubmitTx() { @@ -255,6 +249,6 @@ func (s *KeeperTestSuite) TestWithdrawalBalanceCallback_FailedSubmitTx() { s.App.StakeibcKeeper.SetHostZone(s.Ctx, badHostZone) err := stakeibckeeper.WithdrawalBalanceCallback(s.App.StakeibcKeeper, s.Ctx, tc.validArgs.callbackArgs, tc.validArgs.query) - s.Require().ErrorContains(err, "Failed to SubmitTxs for GAIA - connection-X") + s.Require().ErrorContains(err, "Failed to SubmitTxs") s.Require().ErrorContains(err, "invalid connection id, connection-X not found") } diff --git a/x/stakeibc/keeper/keeper.go b/x/stakeibc/keeper/keeper.go index 224d6f4a2c..8d0a580c09 100644 --- a/x/stakeibc/keeper/keeper.go +++ b/x/stakeibc/keeper/keeper.go @@ -227,7 +227,7 @@ func (k Keeper) IsWithinBufferWindow(ctx sdk.Context) (bool, error) { inWindow := elapsedShareOfEpoch.GT(epochShareThresh) if !inWindow { - k.Logger(ctx).Error(fmt.Sprintf("ICQCB: We're %d pct through the epoch, ICQ cutoff is %d", elapsedShareOfEpoch, epochShareThresh)) + k.Logger(ctx).Error(fmt.Sprintf("Outside ICQ Callback Window. We're %d pct through the epoch, ICQ cutoff is %d", elapsedShareOfEpoch, epochShareThresh)) } return inWindow, nil } diff --git a/x/stakeibc/keeper/msg_server_submit_tx.go b/x/stakeibc/keeper/msg_server_submit_tx.go index 46aa5e396a..c1b2236c81 100644 --- a/x/stakeibc/keeper/msg_server_submit_tx.go +++ b/x/stakeibc/keeper/msg_server_submit_tx.go @@ -42,8 +42,8 @@ func (k Keeper) DelegateOnHost(ctx sdk.Context, hostZone types.HostZone, amt sdk } // Fetch the relevant ICA - delegationIca := hostZone.DelegationAccount - if delegationIca == nil || delegationIca.Address == "" { + delegationAccount := hostZone.DelegationAccount + if delegationAccount == nil || delegationAccount.Address == "" { k.Logger(ctx).Error(fmt.Sprintf("Zone %s is missing a delegation address!", hostZone.ChainId)) return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "Invalid delegation account") } @@ -61,7 +61,7 @@ func (k Keeper) DelegateOnHost(ctx sdk.Context, hostZone types.HostZone, amt sdk relativeAmount := sdk.NewCoin(amt.Denom, targetDelegatedAmts[validator.Address]) if relativeAmount.Amount.IsPositive() { msgs = append(msgs, &stakingTypes.MsgDelegate{ - DelegatorAddress: delegationIca.Address, + DelegatorAddress: delegationAccount.Address, ValidatorAddress: validator.Address, Amount: relativeAmount, }) @@ -86,7 +86,7 @@ func (k Keeper) DelegateOnHost(ctx sdk.Context, hostZone types.HostZone, amt sdk } // Send the transaction through SubmitTx - _, err = k.SubmitTxsStrideEpoch(ctx, connectionId, msgs, *delegationIca, ICACallbackID_Delegate, marshalledCallbackArgs) + _, err = k.SubmitTxsStrideEpoch(ctx, connectionId, msgs, *delegationAccount, ICACallbackID_Delegate, marshalledCallbackArgs) if err != nil { return sdkerrors.Wrapf(err, "Failed to SubmitTxs for connectionId %s on %s. Messages: %s", connectionId, hostZone.ChainId, msgs) } @@ -112,28 +112,27 @@ func (k Keeper) SetWithdrawalAddressOnHost(ctx sdk.Context, hostZone types.HostZ } // Fetch the relevant ICA - delegationIca := hostZone.DelegationAccount - if delegationIca == nil || delegationIca.Address == "" { + delegationAccount := hostZone.DelegationAccount + if delegationAccount == nil || delegationAccount.Address == "" { k.Logger(ctx).Error(fmt.Sprintf("Zone %s is missing a delegation address!", hostZone.ChainId)) return nil } - withdrawalIca := hostZone.WithdrawalAccount - if withdrawalIca == nil || withdrawalIca.Address == "" { + withdrawalAccount := hostZone.WithdrawalAccount + if withdrawalAccount == nil || withdrawalAccount.Address == "" { k.Logger(ctx).Error(fmt.Sprintf("Zone %s is missing a withdrawal address!", hostZone.ChainId)) return nil } - withdrawalIcaAddr := hostZone.WithdrawalAccount.Address - - k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Withdrawal Address: %s, Delegator Address: %s", withdrawalIcaAddr, delegationIca.Address)) + k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Withdrawal Address: %s, Delegator Address: %s", + withdrawalAccount.Address, delegationAccount.Address)) // Construct the ICA message msgs := []sdk.Msg{ &distributiontypes.MsgSetWithdrawAddress{ - DelegatorAddress: delegationIca.Address, - WithdrawAddress: withdrawalIcaAddr, + DelegatorAddress: delegationAccount.Address, + WithdrawAddress: withdrawalAccount.Address, }, } - _, err = k.SubmitTxsStrideEpoch(ctx, connectionId, msgs, *delegationIca, "", nil) + _, err = k.SubmitTxsStrideEpoch(ctx, connectionId, msgs, *delegationAccount, "", nil) if err != nil { return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "Failed to SubmitTxs for %s, %s, %s", connectionId, hostZone.ChainId, msgs) } @@ -141,28 +140,33 @@ func (k Keeper) SetWithdrawalAddressOnHost(ctx sdk.Context, hostZone types.HostZ return nil } -// Simple balance query helper using new ICQ module +// Submits an ICQ for the withdrawal account balance func (k Keeper) UpdateWithdrawalBalance(ctx sdk.Context, hostZone types.HostZone) error { k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Submitting ICQ for withdrawal account balance")) - withdrawalIca := hostZone.WithdrawalAccount - if withdrawalIca == nil || withdrawalIca.Address == "" { - k.Logger(ctx).Error(fmt.Sprintf("Zone %s is missing a withdrawal address!", hostZone.ChainId)) + // Get the withdrawal account address from the host zone + withdrawalAccount := hostZone.WithdrawalAccount + if withdrawalAccount == nil || withdrawalAccount.Address == "" { + return sdkerrors.Wrapf(types.ErrICAAccountNotFound, "no withdrawal account found for %s", hostZone.ChainId) } - _, addr, _ := bech32.DecodeAndConvert(withdrawalIca.Address) - data := bankTypes.CreateAccountBalancesPrefix(addr) + // Encode the withdrawal account address for the query request + // The query request consists of the withdrawal account address and denom + _, withdrawalAddressBz, err := bech32.DecodeAndConvert(withdrawalAccount.Address) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid withdrawal account address, could not decode (%s)", err.Error()) + } + queryData := append(bankTypes.CreateAccountBalancesPrefix(withdrawalAddressBz), []byte(hostZone.HostDenom)...) - // get ttl, the end of the ICA buffer window - epochType := epochstypes.STRIDE_EPOCH - ttl, err := k.GetICATimeoutNanos(ctx, epochType) + // The query should timeout at the end of the ICA buffer window + ttl, err := k.GetICATimeoutNanos(ctx, epochstypes.STRIDE_EPOCH) if err != nil { - errMsg := fmt.Sprintf("Failed to get ICA timeout nanos for epochType %s using param, error: %s", epochType, err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg) + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, + "Failed to get ICA timeout nanos for epochType %s using param, error: %s", epochstypes.STRIDE_EPOCH, err.Error()) } - err = k.InterchainQueryKeeper.MakeRequest( + // Submit the ICQ for the withdrawal account balance + if err := k.InterchainQueryKeeper.MakeRequest( ctx, types.ModuleName, ICQCallbackID_WithdrawalBalance, @@ -171,13 +175,13 @@ func (k Keeper) UpdateWithdrawalBalance(ctx sdk.Context, hostZone types.HostZone // use "bank" store to access acct balances which live in the bank module // use "key" suffix to retrieve a proof alongside the query result icqtypes.BANK_STORE_QUERY_WITH_PROOF, - append(data, []byte(hostZone.HostDenom)...), - ttl, // ttl - ) - if err != nil { + queryData, + ttl, + ); err != nil { k.Logger(ctx).Error(fmt.Sprintf("Error querying for withdrawal balance, error: %s", err.Error())) return err } + return nil } @@ -353,21 +357,22 @@ func (k Keeper) GetLightClientTimeSafely(ctx sdk.Context, connectionID string) ( } } -// query and update validator exchange rate +// Submits an ICQ to get a validator's exchange rate func (k Keeper) QueryValidatorExchangeRate(ctx sdk.Context, msg *types.MsgUpdateValidatorSharesExchRate) (*types.MsgUpdateValidatorSharesExchRateResponse, error) { - // ensure ICQ can be issued now! else fail the callback - valid, err := k.IsWithinBufferWindow(ctx) + k.Logger(ctx).Info(utils.LogWithHostZone(msg.ChainId, "Submitting ICQ for validator exchange rate to %s", msg.Valoper)) + + // Ensure ICQ can be issued now! else fail the callback + withinBufferWindow, err := k.IsWithinBufferWindow(ctx) if err != nil { - return nil, err - } else if !valid { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "unable to determine if ICQ callback is inside buffer window, err: %s", err.Error()) + } else if !withinBufferWindow { return nil, sdkerrors.Wrapf(types.ErrOutsideIcqWindow, "outside the buffer time during which ICQs are allowed (%s)", msg.ChainId) } + // Confirm the host zone exists hostZone, found := k.GetHostZone(ctx, msg.ChainId) if !found { - errMsg := fmt.Sprintf("Host zone not found (%s)", msg.ChainId) - k.Logger(ctx).Error(errMsg) - return nil, sdkerrors.Wrapf(types.ErrInvalidHostZone, errMsg) + return nil, sdkerrors.Wrapf(types.ErrInvalidHostZone, "Host zone not found (%s)", msg.ChainId) } // check that the validator address matches the bech32 prefix of the hz @@ -375,22 +380,21 @@ func (k Keeper) QueryValidatorExchangeRate(ctx sdk.Context, msg *types.MsgUpdate return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "validator operator address must match the host zone bech32 prefix") } - _, valAddr, err := bech32.DecodeAndConvert(msg.Valoper) + // Encode the validator address to form the query request + _, validatorAddressBz, err := bech32.DecodeAndConvert(msg.Valoper) if err != nil { return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid validator operator address, could not decode (%s)", err.Error()) } - data := stakingtypes.GetValidatorKey(valAddr) + queryData := stakingtypes.GetValidatorKey(validatorAddressBz) - // get ttl + // The query should timeout at the start of the next epoch ttl, err := k.GetStartTimeNextEpoch(ctx, epochstypes.STRIDE_EPOCH) if err != nil { - errMsg := fmt.Sprintf("could not get start time for next epoch: %s", err.Error()) - k.Logger(ctx).Error(errMsg) - return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg) + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "could not get start time for next epoch: %s", err.Error()) } - k.Logger(ctx).Info(fmt.Sprintf("Querying validator %v, key %v, denom %v", msg.Valoper, icqtypes.STAKING_STORE_QUERY_WITH_PROOF, hostZone.ChainId)) - err = k.InterchainQueryKeeper.MakeRequest( + // Submit validator exchange rate ICQ + if err := k.InterchainQueryKeeper.MakeRequest( ctx, types.ModuleName, ICQCallbackID_Validator, @@ -399,47 +403,51 @@ func (k Keeper) QueryValidatorExchangeRate(ctx sdk.Context, msg *types.MsgUpdate // use "staking" store to access validator which lives in the staking module // use "key" suffix to retrieve a proof alongside the query result icqtypes.STAKING_STORE_QUERY_WITH_PROOF, - data, - ttl, // ttl - ) - if err != nil { - k.Logger(ctx).Error(fmt.Sprintf("Error querying for validator, error %s", err.Error())) + queryData, + ttl, + ); err != nil { + k.Logger(ctx).Error(fmt.Sprintf("Error submitting ICQ for validator exchange rate, error %s", err.Error())) return nil, err } return &types.MsgUpdateValidatorSharesExchRateResponse{}, nil } -// to icq delegation amounts, this fn is executed after validator exch rates are icq'd +// Submits an ICQ to get a validator's delegations +// This is called after the validator's exchange rate is determined func (k Keeper) QueryDelegationsIcq(ctx sdk.Context, hostZone types.HostZone, valoper string) error { - // ensure ICQ can be issued now! else fail the callback + k.Logger(ctx).Info(utils.LogWithHostZone(hostZone.ChainId, "Submitting ICQ for delegations to %s", valoper)) + + // Ensure ICQ can be issued now! else fail the callback valid, err := k.IsWithinBufferWindow(ctx) if err != nil { - return err + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "unable to determine if ICQ callback is inside buffer window, err: %s", err.Error()) } else if !valid { return sdkerrors.Wrapf(types.ErrOutsideIcqWindow, "outside the buffer time during which ICQs are allowed (%s)", hostZone.HostDenom) } - delegationIca := hostZone.GetDelegationAccount() - if delegationIca == nil || delegationIca.GetAddress() == "" { - errMsg := fmt.Sprintf("Zone %s is missing a delegation address!", hostZone.ChainId) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(types.ErrICAAccountNotFound, errMsg) + // Get the validator and delegator encoded addresses to form the query request + delegationAccount := hostZone.DelegationAccount + if delegationAccount == nil || delegationAccount.Address == "" { + return sdkerrors.Wrapf(types.ErrICAAccountNotFound, "no delegation address found for %s", hostZone.ChainId) + } + _, validatorAddressBz, err := bech32.DecodeAndConvert(valoper) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid validator address, could not decode (%s)", err.Error()) + } + _, delegatorAddressBz, err := bech32.DecodeAndConvert(delegationAccount.Address) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid delegator address, could not decode (%s)", err.Error()) } - delegationAcctAddr := delegationIca.GetAddress() - _, valAddr, _ := bech32.DecodeAndConvert(valoper) - _, delAddr, _ := bech32.DecodeAndConvert(delegationAcctAddr) - data := stakingtypes.GetDelegationKey(delAddr, valAddr) + queryData := stakingtypes.GetDelegationKey(delegatorAddressBz, validatorAddressBz) - // get ttl + // The query should timeout at the start of the next epoch ttl, err := k.GetStartTimeNextEpoch(ctx, epochstypes.STRIDE_EPOCH) if err != nil { - errMsg := fmt.Sprintf("could not get start time for next epoch: %s", err.Error()) - k.Logger(ctx).Error(errMsg) - return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, errMsg) + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "could not get start time for next epoch: %s", err.Error()) } - k.Logger(ctx).Info(fmt.Sprintf("Querying delegation for %s on %s", delegationAcctAddr, valoper)) - err = k.InterchainQueryKeeper.MakeRequest( + // Submit delegator shares ICQ + if err := k.InterchainQueryKeeper.MakeRequest( ctx, types.ModuleName, ICQCallbackID_Delegation, @@ -448,12 +456,12 @@ func (k Keeper) QueryDelegationsIcq(ctx sdk.Context, hostZone types.HostZone, va // use "staking" store to access delegation which lives in the staking module // use "key" suffix to retrieve a proof alongside the query result icqtypes.STAKING_STORE_QUERY_WITH_PROOF, - data, - ttl, // ttl - ) - if err != nil { - k.Logger(ctx).Error(fmt.Sprintf("Error querying for delegation, error : %s", err.Error())) + queryData, + ttl, + ); err != nil { + k.Logger(ctx).Error(fmt.Sprintf("Error submitting ICQ for delegation, error : %s", err.Error())) return err } + return nil } diff --git a/x/stakeibc/keeper/update_validator_shares_exch_rate_test.go b/x/stakeibc/keeper/update_validator_shares_exch_rate_test.go index 65fbbc3adf..a5b7f43e3c 100644 --- a/x/stakeibc/keeper/update_validator_shares_exch_rate_test.go +++ b/x/stakeibc/keeper/update_validator_shares_exch_rate_test.go @@ -246,7 +246,7 @@ func (s *KeeperTestSuite) TestQueryDelegationsIcq_MissingDelegationAddress() { s.App.StakeibcKeeper.SetHostZone(s.Ctx, tc.hostZone) err := s.App.StakeibcKeeper.QueryDelegationsIcq(s.Ctx, tc.hostZone, tc.valoperAddr) - s.Require().ErrorContains(err, "missing a delegation address") + s.Require().ErrorContains(err, "no delegation address found for") } func (s *KeeperTestSuite) TestQueryDelegationsIcq_MissingConnectionId() { diff --git a/x/stakeibc/types/errors.go b/x/stakeibc/types/errors.go index d2de73ee69..6b7a3ff3fb 100644 --- a/x/stakeibc/types/errors.go +++ b/x/stakeibc/types/errors.go @@ -42,7 +42,7 @@ var ( ErrICATxFailed = sdkerrors.Register(ModuleName, 1532, "failed to submit ICA transaction") ErrICQFailed = sdkerrors.Register(ModuleName, 1533, "failed to submit ICQ") ErrDivisionByZero = sdkerrors.Register(ModuleName, 1534, "division by zero") - ErrSlashGtTenPct = sdkerrors.Register(ModuleName, 1535, "slash is greater than 10 percent") + ErrSlashExceedsSafetyThreshold = sdkerrors.Register(ModuleName, 1535, "slash is greater than safety threshold") ErrInvalidEpoch = sdkerrors.Register(ModuleName, 1536, "invalid epoch tracker") ErrHostZoneICAAccountNotFound = sdkerrors.Register(ModuleName, 1537, "host zone's ICA account not found") ErrNoValidatorAmts = sdkerrors.Register(ModuleName, 1538, "could not fetch validator amts") diff --git a/x/stakeibc/types/params.go b/x/stakeibc/types/params.go index 11bdbac30a..9e6f09b410 100644 --- a/x/stakeibc/types/params.go +++ b/x/stakeibc/types/params.go @@ -15,7 +15,7 @@ var ( DefaultReinvestInterval uint64 = 1 DefaultRewardsInterval uint64 = 1 DefaultRedemptionRateInterval uint64 = 1 - // you apparantly cannot safely encode floats, so we make commission / 100 + // you apparently cannot safely encode floats, so we make commission / 100 DefaultStrideCommission uint64 = 10 DefaultValidatorRebalancingThreshold uint64 = 100 // divide by 10,000, so 100 = 1% DefaultICATimeoutNanos uint64 = 600000000000 @@ -27,6 +27,7 @@ var ( DefaultMaxStakeICACallsPerEpoch uint64 = 100 DefaultIBCTransferTimeoutNanos uint64 = 1800000000000 // 30 minutes DefaultSafetyNumValidators uint64 = 35 + DefaultSafetyMaxSlashPercent uint64 = 10 // KeyDepositInterval is store's key for the DepositInterval option KeyDepositInterval = []byte("DepositInterval") @@ -45,6 +46,7 @@ var ( KeyMaxStakeICACallsPerEpoch = []byte("MaxStakeICACallsPerEpoch") KeyIBCTransferTimeoutNanos = []byte("IBCTransferTimeoutNanos") KeySafetyNumValidators = []byte("SafetyNumValidators") + KeySafetyMaxSlashPercent = []byte("SafetyMaxSlashPercent") ) var _ paramtypes.ParamSet = (*Params)(nil) @@ -56,40 +58,42 @@ func ParamKeyTable() paramtypes.KeyTable { // NewParams creates a new Params instance func NewParams( - deposit_interval uint64, - delegate_interval uint64, - rewards_interval uint64, - redemption_rate_interval uint64, - stride_commission uint64, - reinvest_interval uint64, - validator_rebalancing_threshold uint64, - ica_timeout_nanos uint64, - buffer_size uint64, - ibc_timeout_blocks uint64, - fee_transfer_timeout_nanos uint64, - max_stake_ica_calls_per_epoch uint64, - safety_min_redemption_rate_threshold uint64, - safety_max_redemption_rate_threshold uint64, - ibc_transfer_timeout_nanos uint64, - safety_num_validators uint64, + depositInterval uint64, + delegateInterval uint64, + rewardsInterval uint64, + redemptionRateInterval uint64, + strideCommission uint64, + reinvestInterval uint64, + validatorRebalancingThreshold uint64, + icaTimeoutNanos uint64, + bufferSize uint64, + ibcTimeoutBlocks uint64, + feeTransferTimeoutNanos uint64, + maxStakeIcaCallsPerEpoch uint64, + safetyMinRedemptionRateThreshold uint64, + safetyMaxRedemptionRateThreshold uint64, + ibcTransferTimeoutNanos uint64, + safetyNumValidators uint64, + safetyMaxSlashPercent uint64, ) Params { return Params{ - DepositInterval: deposit_interval, - DelegateInterval: delegate_interval, - RewardsInterval: rewards_interval, - RedemptionRateInterval: redemption_rate_interval, - StrideCommission: stride_commission, - ReinvestInterval: reinvest_interval, - ValidatorRebalancingThreshold: validator_rebalancing_threshold, - IcaTimeoutNanos: ica_timeout_nanos, - BufferSize: buffer_size, - IbcTimeoutBlocks: ibc_timeout_blocks, - FeeTransferTimeoutNanos: fee_transfer_timeout_nanos, - MaxStakeIcaCallsPerEpoch: max_stake_ica_calls_per_epoch, - SafetyMinRedemptionRateThreshold: safety_min_redemption_rate_threshold, - SafetyMaxRedemptionRateThreshold: safety_max_redemption_rate_threshold, - IbcTransferTimeoutNanos: ibc_transfer_timeout_nanos, - SafetyNumValidators: safety_num_validators, + DepositInterval: depositInterval, + DelegateInterval: delegateInterval, + RewardsInterval: rewardsInterval, + RedemptionRateInterval: redemptionRateInterval, + StrideCommission: strideCommission, + ReinvestInterval: reinvestInterval, + ValidatorRebalancingThreshold: validatorRebalancingThreshold, + IcaTimeoutNanos: icaTimeoutNanos, + BufferSize: bufferSize, + IbcTimeoutBlocks: ibcTimeoutBlocks, + FeeTransferTimeoutNanos: feeTransferTimeoutNanos, + MaxStakeIcaCallsPerEpoch: maxStakeIcaCallsPerEpoch, + SafetyMinRedemptionRateThreshold: safetyMinRedemptionRateThreshold, + SafetyMaxRedemptionRateThreshold: safetyMaxRedemptionRateThreshold, + IbcTransferTimeoutNanos: ibcTransferTimeoutNanos, + SafetyNumValidators: safetyNumValidators, + SafetyMaxSlashPercent: safetyMaxSlashPercent, } } @@ -112,6 +116,7 @@ func DefaultParams() Params { DefaultSafetyMaxRedemptionRateThreshold, DefaultIBCTransferTimeoutNanos, DefaultSafetyNumValidators, + DefaultSafetyMaxSlashPercent, ) } @@ -134,6 +139,7 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { paramtypes.NewParamSetPair(KeySafetyMaxRedemptionRateThreshold, &p.SafetyMaxRedemptionRateThreshold, validMaxRedemptionRateThreshold), paramtypes.NewParamSetPair(KeyIBCTransferTimeoutNanos, &p.IbcTransferTimeoutNanos, validTimeoutNanos), paramtypes.NewParamSetPair(KeySafetyNumValidators, &p.SafetyNumValidators, isPositive), + paramtypes.NewParamSetPair(KeySafetyMaxSlashPercent, &p.SafetyMaxSlashPercent, validSlashPercent), } } @@ -200,6 +206,18 @@ func validMinRedemptionRateThreshold(i interface{}) error { return nil } +func validSlashPercent(i interface{}) error { + ival, ok := i.(uint64) + if !ok { + return fmt.Errorf("parameter not accepted: %T", i) + } + if ival > 100 { + return fmt.Errorf("parameter must be between 0 and 100: %d", ival) + } + + return nil +} + func isPositive(i interface{}) error { ival, ok := i.(uint64) if !ok { diff --git a/x/stakeibc/types/params.pb.go b/x/stakeibc/types/params.pb.go index d04cb9c05e..3f9a651a1d 100644 --- a/x/stakeibc/types/params.pb.go +++ b/x/stakeibc/types/params.pb.go @@ -48,6 +48,7 @@ type Params struct { SafetyMaxRedemptionRateThreshold uint64 `protobuf:"varint,15,opt,name=safety_max_redemption_rate_threshold,json=safetyMaxRedemptionRateThreshold,proto3" json:"safety_max_redemption_rate_threshold,omitempty"` IbcTransferTimeoutNanos uint64 `protobuf:"varint,16,opt,name=ibc_transfer_timeout_nanos,json=ibcTransferTimeoutNanos,proto3" json:"ibc_transfer_timeout_nanos,omitempty"` SafetyNumValidators uint64 `protobuf:"varint,17,opt,name=safety_num_validators,json=safetyNumValidators,proto3" json:"safety_num_validators,omitempty"` + SafetyMaxSlashPercent uint64 `protobuf:"varint,18,opt,name=safety_max_slash_percent,json=safetyMaxSlashPercent,proto3" json:"safety_max_slash_percent,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -201,6 +202,13 @@ func (m *Params) GetSafetyNumValidators() uint64 { return 0 } +func (m *Params) GetSafetyMaxSlashPercent() uint64 { + if m != nil { + return m.SafetyMaxSlashPercent + } + return 0 +} + func init() { proto.RegisterType((*Params)(nil), "stride.stakeibc.Params") proto.RegisterMapType((map[string]string)(nil), "stride.stakeibc.Params.ZoneComAddressEntry") @@ -209,47 +217,49 @@ func init() { func init() { proto.RegisterFile("stride/stakeibc/params.proto", fileDescriptor_5aeaab6a38c2b438) } var fileDescriptor_5aeaab6a38c2b438 = []byte{ - // 636 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0x4f, 0x4f, 0xdb, 0x4e, - 0x10, 0x4d, 0xf8, 0xf7, 0x83, 0xe5, 0x57, 0x48, 0x0c, 0x6d, 0xad, 0xa8, 0x04, 0x54, 0xf5, 0x00, - 0xa5, 0x4d, 0x54, 0xda, 0x03, 0x82, 0x43, 0x05, 0x88, 0x4a, 0xa8, 0x2d, 0x42, 0x86, 0xf6, 0xc0, - 0x65, 0x35, 0xb6, 0x27, 0xc9, 0x0a, 0x7b, 0xd7, 0xda, 0xdd, 0xa4, 0x49, 0x3e, 0x45, 0x7b, 0xeb, - 0xb1, 0x1f, 0xa7, 0x47, 0x8e, 0x3d, 0x56, 0xf0, 0x45, 0x2a, 0xef, 0x3a, 0x36, 0x41, 0x69, 0x6f, - 0xeb, 0xf7, 0xde, 0x3c, 0xcf, 0x3c, 0xcd, 0x2e, 0x79, 0xa2, 0xb4, 0x64, 0x21, 0x36, 0x95, 0x86, - 0x2b, 0x64, 0x7e, 0xd0, 0x4c, 0x40, 0x42, 0xac, 0x1a, 0x89, 0x14, 0x5a, 0x38, 0xcb, 0x96, 0x6d, - 0x8c, 0xd8, 0xda, 0x6a, 0x5b, 0xb4, 0x85, 0xe1, 0x9a, 0xe9, 0xc9, 0xca, 0x9e, 0x7e, 0x9b, 0x27, - 0x73, 0x67, 0xa6, 0xce, 0xd9, 0x22, 0x15, 0x89, 0x5f, 0x40, 0x86, 0x8a, 0x32, 0xae, 0x51, 0xf6, - 0x20, 0x72, 0xcb, 0x1b, 0xe5, 0xcd, 0x19, 0x6f, 0x39, 0xc3, 0x4f, 0x32, 0xd8, 0xd9, 0x26, 0xd5, - 0x10, 0x23, 0x6c, 0x83, 0xc6, 0x42, 0x3b, 0x67, 0xb4, 0x95, 0x11, 0x91, 0x8b, 0xb7, 0x48, 0x25, - 0xc4, 0x44, 0x28, 0xa6, 0x0b, 0xed, 0x94, 0xf5, 0xcd, 0xf0, 0x5c, 0xba, 0x4b, 0x5c, 0x89, 0x21, - 0xc6, 0x89, 0x66, 0x82, 0x53, 0x39, 0x66, 0x3f, 0x6d, 0x4a, 0x1e, 0x15, 0xbc, 0x77, 0xf7, 0x27, - 0xdb, 0xa4, 0x6a, 0x07, 0xa6, 0x81, 0x88, 0x63, 0xa6, 0x14, 0x13, 0xdc, 0x9d, 0xb1, 0x1d, 0x59, - 0xe2, 0x28, 0xc7, 0x9d, 0x4f, 0xa4, 0x32, 0x14, 0xdc, 0x48, 0x29, 0x84, 0xa1, 0x44, 0xa5, 0xdc, - 0xd9, 0x8d, 0xe9, 0xcd, 0xc5, 0x9d, 0xed, 0xc6, 0xbd, 0xd8, 0x1a, 0x36, 0x9c, 0xc6, 0xa5, 0xe0, - 0xa9, 0xc3, 0x81, 0x55, 0x1f, 0x73, 0x2d, 0x07, 0xde, 0xd2, 0x70, 0x0c, 0x4c, 0x7b, 0x90, 0xc8, - 0x78, 0x0f, 0xd5, 0x9d, 0x49, 0xff, 0xb3, 0x3d, 0x8c, 0x88, 0xbc, 0xe1, 0x77, 0x64, 0xbd, 0x07, - 0x11, 0x0b, 0x41, 0x0b, 0x49, 0x25, 0xfa, 0x10, 0x01, 0x0f, 0x18, 0x6f, 0x53, 0xdd, 0x91, 0xa8, - 0x3a, 0x22, 0x0a, 0xdd, 0x79, 0x53, 0xba, 0x96, 0xcb, 0xbc, 0x42, 0x75, 0x31, 0x12, 0x39, 0xcf, - 0x49, 0x95, 0x05, 0x40, 0x35, 0x8b, 0x51, 0x74, 0x35, 0xe5, 0xc0, 0x85, 0x72, 0x17, 0x6c, 0xbc, - 0x2c, 0x80, 0x0b, 0x8b, 0x9f, 0xa6, 0xb0, 0xb3, 0x4e, 0x16, 0xfd, 0x6e, 0xab, 0x85, 0x92, 0x2a, - 0x36, 0x44, 0x97, 0x18, 0x15, 0xb1, 0xd0, 0x39, 0x1b, 0xa2, 0xf3, 0x82, 0x38, 0xcc, 0x0f, 0x72, - 0x33, 0x3f, 0x12, 0xc1, 0x95, 0x72, 0x17, 0xed, 0x08, 0xcc, 0x0f, 0x32, 0xb7, 0x43, 0x83, 0x3b, - 0xfb, 0xa4, 0xd6, 0x42, 0xa4, 0x5a, 0x02, 0x57, 0xa9, 0xe9, 0x78, 0x0f, 0xff, 0x9b, 0xaa, 0xc7, - 0x2d, 0xc4, 0x8b, 0x4c, 0x30, 0xd6, 0xcb, 0x5b, 0xb2, 0x16, 0x43, 0x9f, 0x9a, 0x9c, 0x69, 0x3a, - 0x41, 0x00, 0x51, 0xa4, 0x68, 0x82, 0x92, 0x62, 0x22, 0x82, 0x8e, 0xfb, 0xc0, 0xd4, 0xbb, 0x31, - 0xf4, 0xcf, 0x53, 0xcd, 0x49, 0x00, 0x47, 0xa9, 0xe2, 0x0c, 0xe5, 0x71, 0xca, 0x3b, 0xa7, 0xe4, - 0x99, 0x82, 0x16, 0xea, 0x01, 0x8d, 0x19, 0xa7, 0xf7, 0xd7, 0xa6, 0x48, 0x71, 0xc9, 0xf8, 0x6c, - 0x58, 0xed, 0x47, 0xc6, 0xbd, 0xb1, 0x05, 0x2a, 0x82, 0xbc, 0xe3, 0x07, 0xfd, 0x7f, 0xf8, 0x2d, - 0x8f, 0xf9, 0x41, 0xff, 0x6f, 0x7e, 0xfb, 0xa4, 0x66, 0xb2, 0x9c, 0x9c, 0x4e, 0xc5, 0xa6, 0x93, - 0x66, 0x3a, 0x29, 0x9d, 0x1d, 0xf2, 0x30, 0x6b, 0x86, 0x77, 0x63, 0x9a, 0x6f, 0x80, 0x72, 0xab, - 0xa6, 0x6e, 0xc5, 0x92, 0xa7, 0xdd, 0xf8, 0x73, 0x4e, 0xd5, 0x0e, 0xc8, 0xca, 0x84, 0x2d, 0x75, - 0x2a, 0x64, 0xfa, 0x0a, 0x07, 0xe6, 0x26, 0x2f, 0x78, 0xe9, 0xd1, 0x59, 0x25, 0xb3, 0x3d, 0x88, - 0xba, 0x68, 0x6e, 0xe1, 0x82, 0x67, 0x3f, 0xf6, 0xa6, 0x76, 0xcb, 0x7b, 0x33, 0xdf, 0x7f, 0xac, - 0x97, 0x0e, 0xdf, 0xff, 0xbc, 0xa9, 0x97, 0xaf, 0x6f, 0xea, 0xe5, 0xdf, 0x37, 0xf5, 0xf2, 0xd7, - 0xdb, 0x7a, 0xe9, 0xfa, 0xb6, 0x5e, 0xfa, 0x75, 0x5b, 0x2f, 0x5d, 0xbe, 0x6a, 0x33, 0xdd, 0xe9, - 0xfa, 0x8d, 0x40, 0xc4, 0xcd, 0x73, 0x73, 0x51, 0x5e, 0x7e, 0x00, 0x5f, 0x35, 0xb3, 0x97, 0xa8, - 0xf7, 0xa6, 0xd9, 0x2f, 0x9e, 0x23, 0x3d, 0x48, 0x50, 0xf9, 0x73, 0xe6, 0x9d, 0x79, 0xfd, 0x27, - 0x00, 0x00, 0xff, 0xff, 0x84, 0x23, 0x51, 0x47, 0xae, 0x04, 0x00, 0x00, + // 661 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0xcf, 0x4f, 0x13, 0x4f, + 0x1c, 0x6d, 0xf9, 0xf5, 0x85, 0xe1, 0x2b, 0xb4, 0x03, 0xe8, 0xa6, 0x91, 0x42, 0x8c, 0x07, 0x10, + 0x6d, 0x23, 0x9a, 0x48, 0xe0, 0x60, 0x80, 0x60, 0x42, 0x54, 0x42, 0x5a, 0xf4, 0xc0, 0x65, 0x32, + 0xbb, 0xfb, 0x69, 0x3b, 0x61, 0x77, 0x66, 0x33, 0x33, 0xad, 0x6d, 0xff, 0x0a, 0x8f, 0x1e, 0xfd, + 0x73, 0x8c, 0x27, 0x8e, 0x1e, 0x0d, 0xfc, 0x23, 0x66, 0x66, 0xb6, 0xbb, 0x2d, 0x41, 0x6f, 0xdb, + 0xf7, 0xde, 0xe7, 0xf5, 0x7d, 0x5e, 0x66, 0x06, 0x3d, 0x56, 0x5a, 0xb2, 0x10, 0xea, 0x4a, 0xd3, + 0x2b, 0x60, 0x7e, 0x50, 0x4f, 0xa8, 0xa4, 0xb1, 0xaa, 0x25, 0x52, 0x68, 0x81, 0x97, 0x1d, 0x5b, + 0x1b, 0xb1, 0x95, 0xd5, 0xb6, 0x68, 0x0b, 0xcb, 0xd5, 0xcd, 0x97, 0x93, 0x3d, 0xf9, 0x39, 0x8f, + 0xe6, 0xce, 0xed, 0x1c, 0xde, 0x46, 0x25, 0x09, 0x5f, 0xa8, 0x0c, 0x15, 0x61, 0x5c, 0x83, 0xec, + 0xd1, 0xc8, 0x2b, 0x6e, 0x16, 0xb7, 0x66, 0x1a, 0xcb, 0x29, 0x7e, 0x9a, 0xc2, 0x78, 0x07, 0x95, + 0x43, 0x88, 0xa0, 0x4d, 0x35, 0xe4, 0xda, 0x39, 0xab, 0x2d, 0x8d, 0x88, 0x4c, 0xbc, 0x8d, 0x4a, + 0x21, 0x24, 0x42, 0x31, 0x9d, 0x6b, 0xa7, 0x9c, 0x6f, 0x8a, 0x67, 0xd2, 0x3d, 0xe4, 0x49, 0x08, + 0x21, 0x4e, 0x34, 0x13, 0x9c, 0xc8, 0x09, 0xfb, 0x69, 0x3b, 0xf2, 0x30, 0xe7, 0x1b, 0xe3, 0x7f, + 0xb2, 0x83, 0xca, 0x6e, 0x61, 0x12, 0x88, 0x38, 0x66, 0x4a, 0x31, 0xc1, 0xbd, 0x19, 0x97, 0xc8, + 0x11, 0xc7, 0x19, 0x8e, 0x3f, 0xa1, 0xd2, 0x50, 0x70, 0x2b, 0x25, 0x34, 0x0c, 0x25, 0x28, 0xe5, + 0xcd, 0x6e, 0x4e, 0x6f, 0x2d, 0xee, 0xee, 0xd4, 0xee, 0xd4, 0x56, 0x73, 0xe5, 0xd4, 0x2e, 0x05, + 0x37, 0x0e, 0x87, 0x4e, 0x7d, 0xc2, 0xb5, 0x1c, 0x34, 0x96, 0x86, 0x13, 0xa0, 0xc9, 0x20, 0x81, + 0xf1, 0x1e, 0xa8, 0xb1, 0x4d, 0xff, 0x73, 0x19, 0x46, 0x44, 0x16, 0xf8, 0x1d, 0xda, 0xe8, 0xd1, + 0x88, 0x85, 0x54, 0x0b, 0x49, 0x24, 0xf8, 0x34, 0xa2, 0x3c, 0x60, 0xbc, 0x4d, 0x74, 0x47, 0x82, + 0xea, 0x88, 0x28, 0xf4, 0xe6, 0xed, 0xe8, 0x7a, 0x26, 0x6b, 0xe4, 0xaa, 0x8b, 0x91, 0x08, 0x3f, + 0x43, 0x65, 0x16, 0x50, 0xa2, 0x59, 0x0c, 0xa2, 0xab, 0x09, 0xa7, 0x5c, 0x28, 0x6f, 0xc1, 0xd5, + 0xcb, 0x02, 0x7a, 0xe1, 0xf0, 0x33, 0x03, 0xe3, 0x0d, 0xb4, 0xe8, 0x77, 0x5b, 0x2d, 0x90, 0x44, + 0xb1, 0x21, 0x78, 0xc8, 0xaa, 0x90, 0x83, 0x9a, 0x6c, 0x08, 0xf8, 0x39, 0xc2, 0xcc, 0x0f, 0x32, + 0x33, 0x3f, 0x12, 0xc1, 0x95, 0xf2, 0x16, 0xdd, 0x0a, 0xcc, 0x0f, 0x52, 0xb7, 0x23, 0x8b, 0xe3, + 0x03, 0x54, 0x69, 0x01, 0x10, 0x2d, 0x29, 0x57, 0xc6, 0x74, 0x32, 0xc3, 0xff, 0x76, 0xea, 0x51, + 0x0b, 0xe0, 0x22, 0x15, 0x4c, 0x64, 0x79, 0x8b, 0xd6, 0x63, 0xda, 0x27, 0xb6, 0x67, 0x62, 0x36, + 0x08, 0x68, 0x14, 0x29, 0x92, 0x80, 0x24, 0x90, 0x88, 0xa0, 0xe3, 0x3d, 0xb0, 0xf3, 0x5e, 0x4c, + 0xfb, 0x4d, 0xa3, 0x39, 0x0d, 0xe8, 0xb1, 0x51, 0x9c, 0x83, 0x3c, 0x31, 0x3c, 0x3e, 0x43, 0x4f, + 0x15, 0x6d, 0x81, 0x1e, 0x90, 0x98, 0x71, 0x72, 0xf7, 0xd8, 0xe4, 0x2d, 0x2e, 0x59, 0x9f, 0x4d, + 0xa7, 0xfd, 0xc8, 0x78, 0x63, 0xe2, 0x00, 0xe5, 0x45, 0x8e, 0xf9, 0xd1, 0xfe, 0x3f, 0xfc, 0x96, + 0x27, 0xfc, 0x68, 0xff, 0x6f, 0x7e, 0x07, 0xa8, 0x62, 0xbb, 0xbc, 0xbf, 0x9d, 0x92, 0x6b, 0xc7, + 0x74, 0x7a, 0x5f, 0x3b, 0xbb, 0x68, 0x2d, 0x0d, 0xc3, 0xbb, 0x31, 0xc9, 0x4e, 0x80, 0xf2, 0xca, + 0x76, 0x6e, 0xc5, 0x91, 0x67, 0xdd, 0xf8, 0x73, 0x46, 0xe1, 0x37, 0xc8, 0x1b, 0x5b, 0x40, 0x45, + 0x54, 0x75, 0x4c, 0x9d, 0x01, 0x70, 0xed, 0x61, 0x3b, 0xb6, 0x96, 0x85, 0x6e, 0x1a, 0xf6, 0xdc, + 0x91, 0x95, 0x43, 0xb4, 0x72, 0xcf, 0xf1, 0xc6, 0x25, 0x34, 0x7d, 0x05, 0x03, 0xfb, 0x04, 0x2c, + 0x34, 0xcc, 0x27, 0x5e, 0x45, 0xb3, 0x3d, 0x1a, 0x75, 0xc1, 0x5e, 0xdf, 0x85, 0x86, 0xfb, 0xb1, + 0x3f, 0xb5, 0x57, 0xdc, 0x9f, 0xf9, 0xf6, 0x7d, 0xa3, 0x70, 0xf4, 0xfe, 0xc7, 0x4d, 0xb5, 0x78, + 0x7d, 0x53, 0x2d, 0xfe, 0xbe, 0xa9, 0x16, 0xbf, 0xde, 0x56, 0x0b, 0xd7, 0xb7, 0xd5, 0xc2, 0xaf, + 0xdb, 0x6a, 0xe1, 0xf2, 0x65, 0x9b, 0xe9, 0x4e, 0xd7, 0xaf, 0x05, 0x22, 0xae, 0x37, 0xed, 0x0d, + 0x7b, 0xf1, 0x81, 0xfa, 0xaa, 0x9e, 0x3e, 0x61, 0xbd, 0xd7, 0xf5, 0x7e, 0xfe, 0x8e, 0xe9, 0x41, + 0x02, 0xca, 0x9f, 0xb3, 0x0f, 0xd4, 0xab, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x7a, 0x07, + 0x0f, 0xe7, 0x04, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -272,6 +282,13 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.SafetyMaxSlashPercent != 0 { + i = encodeVarintParams(dAtA, i, uint64(m.SafetyMaxSlashPercent)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x90 + } if m.SafetyNumValidators != 0 { i = encodeVarintParams(dAtA, i, uint64(m.SafetyNumValidators)) i-- @@ -451,6 +468,9 @@ func (m *Params) Size() (n int) { if m.SafetyNumValidators != 0 { n += 2 + sovParams(uint64(m.SafetyNumValidators)) } + if m.SafetyMaxSlashPercent != 0 { + n += 2 + sovParams(uint64(m.SafetyMaxSlashPercent)) + } return n } @@ -920,6 +940,25 @@ func (m *Params) Unmarshal(dAtA []byte) error { break } } + case 18: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field SafetyMaxSlashPercent", wireType) + } + m.SafetyMaxSlashPercent = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.SafetyMaxSlashPercent |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipParams(dAtA[iNdEx:])