diff --git a/CHANGELOG.md b/CHANGELOG.md index f7581430c..0925aa67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,11 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### Bug Fixes + +- [719](https://github.com/persistenceOne/pstake-native/pull/719) Fix afterEpoch hooks to take LiquidStake feature + instead of LiquidStakeIBC + ## [v2.8.2] - 2024-01-09 ### Bug Fixes @@ -46,7 +51,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes -- [707](https://github.com/persistenceOne/pstake-native/pull/707) Fix liquidstakeibc redeem edge case for protecting cValue +- [707](https://github.com/persistenceOne/pstake-native/pull/707) Fix liquidstakeibc redeem edge case for protecting + cValue ## [v2.8.0] - 2023-12-20 diff --git a/x/ratesync/keeper/hooks.go b/x/ratesync/keeper/hooks.go index 3e410f586..03dbd6792 100644 --- a/x/ratesync/keeper/hooks.go +++ b/x/ratesync/keeper/hooks.go @@ -70,7 +70,7 @@ func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumb for _, hc := range hcs { if hc.Features.LiquidStake.Enabled && epochIdentifier == types.LiquidStakeEpoch { // Add liquidstakekeeper and do stuff - err := k.ExecuteLiquidStakeRateTx(ctx, hc.Features.LiquidStakeIBC, liquidBondDenom, bondDenom, nas.MintRate, hc.ID, hc.ConnectionID, hc.ICAAccount) + err := k.ExecuteLiquidStakeRateTx(ctx, hc.Features.LiquidStake, liquidBondDenom, bondDenom, nas.MintRate, hc.ID, hc.ConnectionID, hc.ICAAccount) if err != nil { k.Logger(ctx).Error("cannot ExecuteLiquidStakeRateTx for host chain ", "id", hc.ID, diff --git a/x/ratesync/keeper/hooks_test.go b/x/ratesync/keeper/hooks_test.go new file mode 100644 index 000000000..ffb15a316 --- /dev/null +++ b/x/ratesync/keeper/hooks_test.go @@ -0,0 +1,34 @@ +package keeper_test + +import sdk "github.com/cosmos/cosmos-sdk/types" + +func (suite *IntegrationTestSuite) TestPostCValueUpdate() { + keeper, ctx := suite.app.RatesyncKeeper, suite.ctx + _ = createNChain(keeper, ctx, 10) + suite.Require().NoError(keeper.PostCValueUpdate(ctx, "uatom", "stk/uatom", sdk.OneDec())) + hc, _ := keeper.GetHostChain(ctx, 1) + hc.Features.LiquidStakeIBC.Enabled = true + hc.Features.LiquidStakeIBC.Denoms = []string{"*"} + keeper.SetHostChain(ctx, hc) + suite.Require().NoError(keeper.PostCValueUpdate(ctx, "uatom", "stk/uatom", sdk.OneDec())) + + hc.ICAAccount.Address = "InvalidAddr" //outer functions do not return errors + keeper.SetHostChain(ctx, hc) + suite.Require().NoError(keeper.PostCValueUpdate(ctx, "uatom", "stk/uatom", sdk.OneDec())) + +} + +func (suite *IntegrationTestSuite) TestAfterEpochEnd() { + keeper, ctx := suite.app.RatesyncKeeper, suite.ctx + _ = createNChain(keeper, ctx, 10) + suite.Require().NoError(keeper.AfterEpochEnd(ctx, "hour", 1)) + hc, _ := keeper.GetHostChain(ctx, 1) + hc.Features.LiquidStake.Enabled = true + hc.Features.LiquidStake.Denoms = []string{"*"} + keeper.SetHostChain(ctx, hc) + suite.Require().NoError(keeper.AfterEpochEnd(ctx, "hour", 1)) + + hc.ICAAccount.Address = "InvalidAddr" // outer functions do not return errors + keeper.SetHostChain(ctx, hc) + suite.Require().NoError(keeper.AfterEpochEnd(ctx, "hour", 1)) +} diff --git a/x/ratesync/keeper/ibc_test.go b/x/ratesync/keeper/ibc_test.go new file mode 100644 index 000000000..41543f6bf --- /dev/null +++ b/x/ratesync/keeper/ibc_test.go @@ -0,0 +1,181 @@ +package keeper_test + +import ( + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/gogoproto/proto" + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + "github.com/persistenceOne/pstake-native/v2/x/ratesync/keeper" + "github.com/persistenceOne/pstake-native/v2/x/ratesync/types" +) + +func (suite *IntegrationTestSuite) TestOnChanOpenAck() { + k, ctx := suite.app.RatesyncKeeper, suite.ctx + _ = createNChain(k, ctx, 2) + hc, _ := k.GetHostChain(ctx, 1) + suite.Require().NoError(k.OnChanOpenAck(ctx, types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + suite.ratesyncPathAB.EndpointA.ChannelID, "", "")) +} + +func (suite *IntegrationTestSuite) TestOnAcknowledgementPacket() { + k, ctx := suite.app.RatesyncKeeper, suite.ctx + _ = createNChain(k, ctx, 2) + hc, _ := k.GetHostChain(ctx, 1) + //case 1, instantiate msg. + { + msg, memo, err := keeper.GenerateInstantiateLiquidStakeContractMsg(hc.ICAAccount, hc.Features.LiquidStakeIBC, hc.ID) + suite.Require().NoError(err) + msgData, err := icatypes.SerializeCosmosTx(suite.app.AppCodec(), []proto.Message{msg}) + suite.Require().NoError(err) + data := &icatypes.InterchainAccountPacketData{ + Type: 0, + Data: msgData, + Memo: string(memo), + } + databz, err := suite.app.AppCodec().MarshalJSON(data) + suite.Require().NoError(err) + packet := channeltypes.Packet{ + SourcePort: types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + Data: databz, + } + wasmResponse := &wasmtypes.MsgInstantiateContractResponse{ + Address: authtypes.NewModuleAddress("Contract").String(), + Data: nil, + } + + msgResult, _ := codectypes.NewAnyWithValue(wasmResponse) + + result := &sdk.TxMsgData{MsgResponses: []*codectypes.Any{msgResult}} + resultbz, err := suite.app.AppCodec().Marshal(result) + suite.Require().NoError(err) + ack := channeltypes.NewResultAcknowledgement(resultbz) + ackbz, err := suite.app.AppCodec().MarshalJSON(&ack) + suite.Require().NoError(k.OnAcknowledgementPacket(ctx, packet, + ackbz, authtypes.NewModuleAddress("test"))) + } + // case 2 instantiate failure + { + msg, memo, err := keeper.GenerateInstantiateLiquidStakeContractMsg(hc.ICAAccount, hc.Features.LiquidStakeIBC, hc.ID) + suite.Require().NoError(err) + msgData, err := icatypes.SerializeCosmosTx(suite.app.AppCodec(), []proto.Message{msg}) + suite.Require().NoError(err) + data := &icatypes.InterchainAccountPacketData{ + Type: 0, + Data: msgData, + Memo: string(memo), + } + databz, err := suite.app.AppCodec().MarshalJSON(data) + suite.Require().NoError(err) + packet := channeltypes.Packet{ + SourcePort: types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + Data: databz, + } + + ack := channeltypes.NewErrorAcknowledgement(types.ErrICATxFailure) + ackbz, err := suite.app.AppCodec().MarshalJSON(&ack) + suite.Require().NoError(k.OnAcknowledgementPacket(ctx, packet, + ackbz, authtypes.NewModuleAddress("test"))) + } + //case 3, execute msg. + { + msg, memo, err := keeper.GenerateExecuteLiquidStakeRateTxMsg(ctx.BlockTime().Unix(), hc.Features.LiquidStake, + "stk/uatom", "uatom", sdk.OneDec(), hc.ID, hc.ICAAccount) + suite.Require().NoError(err) + msgData, err := icatypes.SerializeCosmosTx(suite.app.AppCodec(), []proto.Message{msg}) + suite.Require().NoError(err) + data := &icatypes.InterchainAccountPacketData{ + Type: 0, + Data: msgData, + Memo: string(memo), + } + databz, err := suite.app.AppCodec().MarshalJSON(data) + suite.Require().NoError(err) + packet := channeltypes.Packet{ + SourcePort: types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + Data: databz, + } + wasmResponse := &wasmtypes.MsgExecuteContractResponse{Data: []byte{}} + + msgResult, _ := codectypes.NewAnyWithValue(wasmResponse) + + result := &sdk.TxMsgData{MsgResponses: []*codectypes.Any{msgResult}} + resultbz, err := suite.app.AppCodec().Marshal(result) + suite.Require().NoError(err) + ack := channeltypes.NewResultAcknowledgement(resultbz) + ackbz, err := suite.app.AppCodec().MarshalJSON(&ack) + suite.Require().NoError(k.OnAcknowledgementPacket(ctx, packet, + ackbz, authtypes.NewModuleAddress("test"))) + } + // case 4 execute failure + { + msg, memo, err := keeper.GenerateExecuteLiquidStakeRateTxMsg(ctx.BlockTime().Unix(), hc.Features.LiquidStake, + "stk/uatom", "uatom", sdk.OneDec(), hc.ID, hc.ICAAccount) + suite.Require().NoError(err) + msgData, err := icatypes.SerializeCosmosTx(suite.app.AppCodec(), []proto.Message{msg}) + suite.Require().NoError(err) + data := &icatypes.InterchainAccountPacketData{ + Type: 0, + Data: msgData, + Memo: string(memo), + } + databz, err := suite.app.AppCodec().MarshalJSON(data) + suite.Require().NoError(err) + packet := channeltypes.Packet{ + SourcePort: types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + Data: databz, + } + + ack := channeltypes.NewErrorAcknowledgement(types.ErrICATxFailure) + ackbz, err := suite.app.AppCodec().MarshalJSON(&ack) + suite.Require().NoError(k.OnAcknowledgementPacket(ctx, packet, + ackbz, authtypes.NewModuleAddress("test"))) + } +} + +func (suite *IntegrationTestSuite) TestOnTimeoutPacket() { + k, ctx := suite.app.RatesyncKeeper, suite.ctx + _ = createNChain(k, ctx, 2) + hc, _ := k.GetHostChain(ctx, 1) + //case 1, instantiate msg. + { + msg, memo, err := keeper.GenerateInstantiateLiquidStakeContractMsg(hc.ICAAccount, hc.Features.LiquidStakeIBC, hc.ID) + suite.Require().NoError(err) + msgData, err := icatypes.SerializeCosmosTx(suite.app.AppCodec(), []proto.Message{msg}) + suite.Require().NoError(err) + data := &icatypes.InterchainAccountPacketData{ + Type: 0, + Data: msgData, + Memo: string(memo), + } + databz, err := suite.app.AppCodec().MarshalJSON(data) + suite.Require().NoError(err) + packet := channeltypes.Packet{ + SourcePort: types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + Data: databz, + } + suite.Require().NoError(k.OnTimeoutPacket(ctx, packet, authtypes.NewModuleAddress("test"))) + } + //case 2, execute msg. + { + msg, memo, err := keeper.GenerateExecuteLiquidStakeRateTxMsg(ctx.BlockTime().Unix(), hc.Features.LiquidStake, + "stk/uatom", "uatom", sdk.OneDec(), hc.ID, hc.ICAAccount) + suite.Require().NoError(err) + msgData, err := icatypes.SerializeCosmosTx(suite.app.AppCodec(), []proto.Message{msg}) + suite.Require().NoError(err) + data := &icatypes.InterchainAccountPacketData{ + Type: 0, + Data: msgData, + Memo: string(memo), + } + databz, err := suite.app.AppCodec().MarshalJSON(data) + suite.Require().NoError(err) + packet := channeltypes.Packet{ + SourcePort: types.MustICAPortIDFromOwner(hc.ICAAccount.Owner), + Data: databz, + } + suite.Require().NoError(k.OnTimeoutPacket(ctx, packet, authtypes.NewModuleAddress("test"))) + } +} diff --git a/x/ratesync/keeper/ica_tx.go b/x/ratesync/keeper/ica_tx.go index 0c5cbc6be..0980a4331 100644 --- a/x/ratesync/keeper/ica_tx.go +++ b/x/ratesync/keeper/ica_tx.go @@ -102,32 +102,9 @@ func (k *Keeper) ExecuteLiquidStakeRateTx(ctx sdk.Context, feature types.LiquidS mintDenom, hostDenom string, cValue sdk.Dec, hostchainId uint64, connectionID string, icaAccount liquidstakeibctypes.ICAAccount) error { if feature.AllowsDenom(mintDenom) { - contractMsg := types.ExecuteLiquidStakeRate{ - LiquidStakeRate: types.LiquidStakeRate{ - DefaultBondDenom: hostDenom, - StkDenom: mintDenom, - CValue: cValue, - ControllerChainTime: ctx.BlockTime().Unix(), - }, - } - contractBz, err := json.Marshal(contractMsg) - if err != nil { - return err - } - msg := &wasmtypes.MsgExecuteContract{ - Sender: icaAccount.Address, - Contract: feature.ContractAddress, - Msg: contractBz, - Funds: nil, - } - memo := types.ICAMemo{ - FeatureType: feature.FeatureType, - HostChainID: hostchainId, - } - memoBz, err := json.Marshal(memo) + msg, memoBz, err := GenerateExecuteLiquidStakeRateTxMsg(ctx.BlockTime().Unix(), feature, mintDenom, hostDenom, cValue, hostchainId, icaAccount) if err != nil { return err - } _, err = k.GenerateAndExecuteICATx(ctx, connectionID, icaAccount.Owner, []proto.Message{msg}, string(memoBz)) if err != nil { @@ -137,15 +114,58 @@ func (k *Keeper) ExecuteLiquidStakeRateTx(ctx sdk.Context, feature types.LiquidS return nil } +func GenerateExecuteLiquidStakeRateTxMsg(blockTime int64, feature types.LiquidStake, + mintDenom, hostDenom string, cValue sdk.Dec, hostchainId uint64, + icaAccount liquidstakeibctypes.ICAAccount) (sdk.Msg, []byte, error) { + contractMsg := types.ExecuteLiquidStakeRate{ + LiquidStakeRate: types.LiquidStakeRate{ + DefaultBondDenom: hostDenom, + StkDenom: mintDenom, + CValue: cValue, + ControllerChainTime: blockTime, + }, + } + contractBz, err := json.Marshal(contractMsg) + if err != nil { + return nil, nil, err + } + msg := &wasmtypes.MsgExecuteContract{ + Sender: icaAccount.Address, + Contract: feature.ContractAddress, + Msg: contractBz, + Funds: nil, + } + memo := types.ICAMemo{ + FeatureType: feature.FeatureType, + HostChainID: hostchainId, + } + memoBz, err := json.Marshal(memo) + if err != nil { + return nil, nil, err + + } + return msg, memoBz, nil +} + func (k *Keeper) InstantiateLiquidStakeContract(ctx sdk.Context, icaAccount liquidstakeibctypes.ICAAccount, feature types.LiquidStake, id uint64, connectionID string) error { // generate contract msg{msg} + msg, memoBz, err := GenerateInstantiateLiquidStakeContractMsg(icaAccount, feature, id) + _, err = k.GenerateAndExecuteICATx(ctx, connectionID, icaAccount.Owner, []proto.Message{msg}, string(memoBz)) + if err != nil { + return err + } + return nil +} + +func GenerateInstantiateLiquidStakeContractMsg(icaAccount liquidstakeibctypes.ICAAccount, + feature types.LiquidStake, id uint64) (sdk.Msg, []byte, error) { contractMsg := types.InstantiateLiquidStakeRateContract{ Admin: icaAccount.Address, } contractMsgBz, err := json.Marshal(contractMsg) if err != nil { - return errorsmod.Wrapf(err, "unable to marshal InstantiateLiquidStakeRateContract") + return nil, nil, errorsmod.Wrapf(err, "unable to marshal InstantiateLiquidStakeRateContract") } msg := &wasmtypes.MsgInstantiateContract{ @@ -162,11 +182,7 @@ func (k *Keeper) InstantiateLiquidStakeContract(ctx sdk.Context, icaAccount liqu } memobz, err := json.Marshal(memo) if err != nil { - return err + return nil, nil, err } - _, err = k.GenerateAndExecuteICATx(ctx, connectionID, icaAccount.Owner, []proto.Message{msg}, string(memobz)) - if err != nil { - return err - } - return nil + return msg, memobz, nil } diff --git a/x/ratesync/keeper/ica_tx_test.go b/x/ratesync/keeper/ica_tx_test.go new file mode 100644 index 000000000..1d26acc2d --- /dev/null +++ b/x/ratesync/keeper/ica_tx_test.go @@ -0,0 +1,15 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (suite *IntegrationTestSuite) TestExecuteLiquidStakeRateTx() { + k, ctx := suite.app.RatesyncKeeper, suite.ctx + _ = createNChain(k, ctx, 2) + hc, _ := k.GetHostChain(ctx, 1) + suite.Require().NoError(k.ExecuteLiquidStakeRateTx(ctx, hc.Features.LiquidStakeIBC, + "stk/uatom", "uatom", sdk.OneDec(), hc.ID, suite.ratesyncPathAB.EndpointA.ConnectionID, hc.ICAAccount)) + suite.Require().NoError(k.InstantiateLiquidStakeContract(ctx, hc.ICAAccount, + hc.Features.LiquidStake, hc.ID, suite.ratesyncPathAB.EndpointA.ConnectionID)) +} diff --git a/x/ratesync/keeper/msg_server_test.go b/x/ratesync/keeper/msg_server_test.go index ac00fe8bd..d439623a0 100644 --- a/x/ratesync/keeper/msg_server_test.go +++ b/x/ratesync/keeper/msg_server_test.go @@ -48,7 +48,21 @@ func (suite *IntegrationTestSuite) TestChainMsgServerUpdate() { hc.ICAAccount.ChannelState = liquidstakeibctypes.ICAAccount_ICA_CHANNEL_CREATED hc.ChainID = ctx.ChainID() k.SetHostChain(ctx, hc) - hc2 := types.HostChain{ID: 1} + hc2 := types.HostChain{ID: 300} + + hc3, _ := k.GetHostChain(ctx, 0) + hc3.ID = 1 + hc3.ChainID = "testchain2-1" + hc3.ConnectionID = suite.ratesyncPathAB.EndpointA.ConnectionID + hc3.ICAAccount.Owner = types.DefaultPortOwner(1) + k.SetHostChain(ctx, hc3) + + hc3.Features.LiquidStake.Instantiation = types.InstantiationState_INSTANTIATION_INITIATED + hc3.Features.LiquidStake.CodeID = 1 + hc4, _ := k.GetHostChain(ctx, hc3.ID) + hc4.Features.LiquidStakeIBC.Instantiation = types.InstantiationState_INSTANTIATION_INITIATED + hc4.Features.LiquidStakeIBC.CodeID = 1 + tests := []struct { desc string request *types.MsgUpdateHostChain @@ -74,6 +88,18 @@ func (suite *IntegrationTestSuite) TestChainMsgServerUpdate() { }, err: sdkerrors.ErrKeyNotFound, }, + { + desc: "Update feature", + request: &types.MsgUpdateHostChain{Authority: GovAddress.String(), + HostChain: hc3, + }, + }, + { + desc: "Update feature2", + request: &types.MsgUpdateHostChain{Authority: GovAddress.String(), + HostChain: hc4, + }, + }, } for _, tc := range tests { suite.T().Run(tc.desc, func(t *testing.T) { @@ -148,3 +174,36 @@ func (suite *IntegrationTestSuite) TestChainMsgServerDelete() { }) } } + +func (suite *IntegrationTestSuite) TestChainMsgServerUpdateParams() { + k, ctx := suite.app.RatesyncKeeper, suite.ctx + + tests := []struct { + desc string + request *types.MsgUpdateParams + err error + }{ + { + desc: "Completed", + request: &types.MsgUpdateParams{Authority: GovAddress.String(), + Params: types.Params{Admin: GovAddress.String()}, + }, + }, + { + desc: "Unauthorized", + request: &types.MsgUpdateParams{Authority: "B", + Params: types.Params{Admin: GovAddress.String()}, + }, + err: sdkerrors.ErrorInvalidSigner, + }, + } + for _, tc := range tests { + suite.T().Run(tc.desc, func(t *testing.T) { + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + _, err := srv.UpdateParams(wctx, tc.request) + suite.Require().ErrorIs(err, tc.err) + }) + } +}