diff --git a/x/interchainstaking/keeper/ibc_packet_handlers.go b/x/interchainstaking/keeper/ibc_packet_handlers.go index 1dfe9d1d0..502026877 100644 --- a/x/interchainstaking/keeper/ibc_packet_handlers.go +++ b/x/interchainstaking/keeper/ibc_packet_handlers.go @@ -1,7 +1,6 @@ package keeper import ( - "encoding/json" "errors" "fmt" "reflect" @@ -60,42 +59,40 @@ func DeserializeCosmosTxTyped(cdc codec.BinaryCodec, data []byte) ([]TypedMsg, e } func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte) error { - ack := channeltypes.Acknowledgement_Result{} - err := json.Unmarshal(acknowledgement, &ack) - txMsgData := &sdk.TxMsgData{} - var success bool + var ( + ack channeltypes.Acknowledgement + success bool + txMsgData sdk.TxMsgData + packetData icatypes.InterchainAccountPacketData + ) + + err := icatypes.ModuleCdc.UnmarshalJSON(acknowledgement, &ack) if err != nil { k.Logger(ctx).Error("unable to unmarshal acknowledgement", "error", err, "data", acknowledgement) return err } - if reflect.DeepEqual(ack, channeltypes.Acknowledgement_Result{}) { - ackErr := channeltypes.Acknowledgement_Error{} - err := json.Unmarshal(acknowledgement, &ackErr) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal acknowledgement error", "error", err, "data", acknowledgement) - return err - } - k.Logger(ctx).Error("received an acknowledgement error", "error", err, "remote_err", ackErr, "data", acknowledgement) + if !ack.Success() { + ackErr := ack.GetError() + k.Logger(ctx).Error("received an acknowledgement error", "remote_err", ackErr, "data", ack.String()) defer telemetry.IncrCounter(1, types.ModuleName, "ica_acknowledgement_errors") success = false } else { defer telemetry.IncrCounter(1, types.ModuleName, "ica_acknowledgement_success") - - err = proto.Unmarshal(ack.Result, txMsgData) + err = proto.Unmarshal(ack.GetResult(), &txMsgData) if err != nil { - k.Logger(ctx).Error("unable to unmarshal acknowledgement", "error", err, "ack", ack.Result) + k.Logger(ctx).Error("unable to unmarshal acknowledgement", "error", err, "ack", ack.GetResult()) return err } success = true } - var packetData icatypes.InterchainAccountPacketData err = icatypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &packetData) if err != nil { k.Logger(ctx).Error("unable to unmarshal acknowledgement packet data", "error", err, "data", packetData) return err } + if reflect.DeepEqual(packetData, icatypes.InterchainAccountPacketData{}) { return errors.New("unable to unmarshal packet data; got empty JSON object") } @@ -109,26 +106,32 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack for msgIndex, msg := range msgs { // use msgData for v0.45 and below and msgResponse for v0.46+ //nolint:staticcheck // SA1019 ignore this! - var msgData *sdk.MsgData var msgResponse []byte - var msgResponseType string - if len(txMsgData.MsgResponses) > 0 { - msgResponseType = txMsgData.MsgResponses[msgIndex].GetTypeUrl() + + // check that the msgResponses slice is at least the length of the current index. + switch { + case !success: + // no-op - there is no msgresponse for a AckErr + case len(txMsgData.MsgResponses) > msgIndex: msgResponse = txMsgData.MsgResponses[msgIndex].GetValue() - } else if len(txMsgData.Data) > 0 { - msgData = txMsgData.Data[msgIndex] + case len(txMsgData.Data) > msgIndex: + msgResponse = txMsgData.Data[msgIndex].GetData() + default: + return fmt.Errorf("could not find msgresponse for index %d", msgIndex) } - src := msg.Msg - switch msg.Type { case "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward": - // TODO: if this fails, it's okay. log but continue. if !success { + withdrawalMsg, ok := msg.Msg.(*distrtypes.MsgWithdrawDelegatorReward) + if !ok { + return errors.New("unable to unmarshal MsgWithdrawDelegatorReward") + } + k.Logger(ctx).Error("Failed to withdraw rewards; will try again next epoch", "validator", withdrawalMsg.ValidatorAddress) return nil } k.Logger(ctx).Info("Rewards withdrawn") - if err := k.HandleWithdrawRewards(ctx, src); err != nil { + if err := k.HandleWithdrawRewards(ctx, msg.Msg); err != nil { return err } continue @@ -139,22 +142,15 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack } response := lsmstakingtypes.MsgRedeemTokensforSharesResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgRedeemTokensforShares response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgRedeemTokensforShares response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unmarshal MsgRedeemTokensforShares response", "error", err) + return err } + k.Logger(ctx).Info("Tokens redeemed for shares", "response", response) // we should update delegation records here. - if err := k.HandleRedeemTokens(ctx, src, response.Amount); err != nil { + if err := k.HandleRedeemTokens(ctx, msg.Msg, response.Amount); err != nil { return err } continue @@ -164,21 +160,15 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack return nil } response := lsmstakingtypes.MsgTokenizeSharesResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgTokenizeShares response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgTokenizeShares response", "error", err) - return err - } + + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgTokenizeShares response", "error", err) + return err } + k.Logger(ctx).Info("Shares tokenized", "response", response) - if err := k.HandleTokenizedShares(ctx, src, response.Amount, packetData.Memo); err != nil { + if err := k.HandleTokenizedShares(ctx, msg.Msg, response.Amount, packetData.Memo); err != nil { return err } continue @@ -188,47 +178,34 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack return nil } response := stakingtypes.MsgDelegateResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgDelegate response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgDelegate response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgDelegate response", "error", err) + return err } + k.Logger(ctx).Info("Delegated", "response", response) // we should update delegation records here. - if err := k.HandleDelegate(ctx, src, packetData.Memo); err != nil { + if err := k.HandleDelegate(ctx, msg.Msg, packetData.Memo); err != nil { return err } continue case "/cosmos.staking.v1beta1.MsgBeginRedelegate": if success { response := stakingtypes.MsgBeginRedelegateResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgBeginRedelegate response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgBeginRedelegate response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + k.Logger(ctx).Info("unmarshalling msgResponse", "response", response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgBeginRedelegate response", "error", err) + return err } + k.Logger(ctx).Info("Redelegation initiated", "response", response) - if err := k.HandleBeginRedelegate(ctx, src, response.CompletionTime, packetData.Memo); err != nil { + if err := k.HandleBeginRedelegate(ctx, msg.Msg, response.CompletionTime, packetData.Memo); err != nil { return err } } else { - if err := k.HandleFailedBeginRedelegate(ctx, src, packetData.Memo); err != nil { + if err := k.HandleFailedBeginRedelegate(ctx, msg.Msg, packetData.Memo); err != nil { return err } } @@ -236,25 +213,18 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack case "/cosmos.staking.v1beta1.MsgUndelegate": if success { response := stakingtypes.MsgUndelegateResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgUndelegate response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgUndelegate response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgUndelegate response", "error", err) + return err } + k.Logger(ctx).Info("Undelegation started", "response", response) - if err := k.HandleUndelegate(ctx, src, response.CompletionTime, packetData.Memo); err != nil { + if err := k.HandleUndelegate(ctx, msg.Msg, response.CompletionTime, packetData.Memo); err != nil { return err } } else { - if err := k.HandleFailedUndelegate(ctx, src, packetData.Memo); err != nil { + if err := k.HandleFailedUndelegate(ctx, msg.Msg, packetData.Memo); err != nil { return err } } @@ -262,29 +232,22 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack case "/cosmos.bank.v1beta1.MsgSend": if !success { - if err := k.HandleFailedBankSend(ctx, src, packetData.Memo); err != nil { + if err := k.HandleFailedBankSend(ctx, msg.Msg, packetData.Memo); err != nil { k.Logger(ctx).Error("unable to handle failed MsgSend", "error", err) return err } continue } response := banktypes.MsgSendResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgSend response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgSend response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgSend response", "error", err) + return err } + k.Logger(ctx).Info("Funds Transferred", "response", response) // check tokenTransfers - if end user unescrow and burn txs - if err := k.HandleCompleteSend(ctx, src, packetData.Memo); err != nil { + if err := k.HandleCompleteSend(ctx, msg.Msg, packetData.Memo); err != nil { return err } case "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress": @@ -293,21 +256,14 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack return nil } response := distrtypes.MsgSetWithdrawAddressResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgSetWithdrawAddress response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgSetWithdrawAddress response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgSetWithdrawAddress response", "error", err) + return err } + k.Logger(ctx).Info("Withdraw Address Updated", "response", response) - if err := k.HandleUpdatedWithdrawAddress(ctx, src); err != nil { + if err := k.HandleUpdatedWithdrawAddress(ctx, msg.Msg); err != nil { return err } case "/ibc.applications.transfer.v1.MsgTransfer": @@ -316,25 +272,18 @@ func (k *Keeper) HandleAcknowledgement(ctx sdk.Context, packet channeltypes.Pack return nil } response := ibctransfertypes.MsgTransferResponse{} - if msgResponseType != "" { - err = proto.Unmarshal(msgResponse, &response) - if err != nil { - k.Logger(ctx).Error("unable to unpack MsgTransfer response", "error", err) - return err - } - } else { - err := proto.Unmarshal(msgData.Data, &response) - if err != nil { - k.Logger(ctx).Error("unable to unmarshal MsgTransfer response", "error", err) - return err - } + err = proto.Unmarshal(msgResponse, &response) + if err != nil { + k.Logger(ctx).Error("unable to unpack MsgTransfer response", "error", err) + return err } + k.Logger(ctx).Info("MsgTranfer acknowledgement received") - if err := k.HandleMsgTransfer(ctx, src); err != nil { + if err := k.HandleMsgTransfer(ctx, msg.Msg); err != nil { return err } default: - k.Logger(ctx).Error("unhandled acknowledgement packet", "type", reflect.TypeOf(src).Name()) + k.Logger(ctx).Error("unhandled acknowledgement packet", "type", reflect.TypeOf(msg.Msg).Name()) } } @@ -515,7 +464,7 @@ func (k *Keeper) HandleMaturedUnbondings(ctx sdk.Context, zone *types.Zone) erro if ctx.BlockTime().After(withdrawal.CompletionTime) && !withdrawal.CompletionTime.Equal(time.Time{}) { // completion date has passed. k.Logger(ctx).Info("found completed unbonding") sendMsg := &banktypes.MsgSend{FromAddress: zone.DelegationAddress.GetAddress(), ToAddress: withdrawal.Recipient, Amount: sdk.Coins{withdrawal.Amount[0]}} - err := k.SubmitTx(ctx, []sdk.Msg{sendMsg}, zone.DelegationAddress, fmt.Sprintf("%s/%s", types.MsgTypeUnbondSend, withdrawal.Txhash), zone.MessagesPerTx) + err := k.SubmitTx(ctx, []sdk.Msg{sendMsg}, zone.DelegationAddress, types.TxUnbondSendMemo(withdrawal.Txhash), zone.MessagesPerTx) if err != nil { k.Logger(ctx).Error("error", err) @@ -575,10 +524,6 @@ func (k *Keeper) HandleTokenizedShares(ctx sdk.Context, msg sdk.Msg, sharesAmoun } func (k *Keeper) HandleBeginRedelegate(ctx sdk.Context, msg sdk.Msg, completion time.Time, memo string) error { - if completion.IsZero() { - return errors.New("invalid zero nil completion time") - } - epochNumber, err := types.ParseEpochMsgMemo(memo, types.MsgTypeRebalance) if err != nil { return err @@ -591,22 +536,30 @@ func (k *Keeper) HandleBeginRedelegate(ctx sdk.Context, msg sdk.Msg, completion return errors.New("unable to unmarshal MsgBeginRedelegate") } zone := k.GetZoneForDelegateAccount(ctx, redelegateMsg.DelegatorAddress) - record, found := k.GetRedelegationRecord(ctx, zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) - if !found { - k.Logger(ctx).Error("unable to find redelegation record", "chain", zone.ChainId, "source", redelegateMsg.ValidatorSrcAddress, "dst", redelegateMsg.ValidatorDstAddress, "epoch_number", epochNumber) - return fmt.Errorf("unable to find redelegation record for chain %s, src: %s, dst: %s, at epoch %d", zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) + + if completion.IsZero() { + // a zero completion time can only happen when the validator is unbonded; this means the redelegation has _already_ completed and can be removed. + k.DeleteRedelegationRecord(ctx, zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) + } else { + + record, found := k.GetRedelegationRecord(ctx, zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) + if !found { + k.Logger(ctx).Error("unable to find redelegation record", "chain", zone.ChainId, "source", redelegateMsg.ValidatorSrcAddress, "dst", redelegateMsg.ValidatorDstAddress, "epoch_number", epochNumber) + return fmt.Errorf("unable to find redelegation record for chain %s, src: %s, dst: %s, at epoch %d", zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) + } + + k.Logger(ctx).Info("updating redelegation record with completion time", "completion", completion) + record.CompletionTime = completion + k.SetRedelegationRecord(ctx, record) } - k.Logger(ctx).Info("updating redelegation record with completion time", "completion", completion) - record.CompletionTime = completion - k.SetRedelegationRecord(ctx, record) tgtDelegation, found := k.GetDelegation(ctx, zone, redelegateMsg.DelegatorAddress, redelegateMsg.ValidatorDstAddress) if !found { - k.Logger(ctx).Error("unable to find delegation record", "chain", zone.ChainId, "source", redelegateMsg.ValidatorSrcAddress, "dst", redelegateMsg.ValidatorDstAddress, "epoch_number", epochNumber) - return fmt.Errorf("unable to find delegation record for chain %s, src: %s, dst: %s, at epoch %d", zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) + tgtDelegation = types.NewDelegation(redelegateMsg.DelegatorAddress, redelegateMsg.ValidatorDstAddress, redelegateMsg.Amount) + } else { + tgtDelegation.Amount = tgtDelegation.Amount.Add(redelegateMsg.Amount) } - // TODO: is the field below actually used? - tgtDelegation.Amount = tgtDelegation.Amount.Add(redelegateMsg.Amount) + // RedelegationEnd is used to determine whether the delegation is 'locked' for transient redelegations. tgtDelegation.RedelegationEnd = completion.Unix() // this field should be a timestamp, but let's avoid unnecessary state changes. k.SetDelegation(ctx, zone, tgtDelegation) @@ -633,12 +586,13 @@ func (k *Keeper) HandleBeginRedelegate(ctx sdk.Context, msg sdk.Msg, completion 0, ) - srcDelegation, found := k.GetDelegation(ctx, zone, redelegateMsg.DelegatorAddress, redelegateMsg.ValidatorDstAddress) + srcDelegation, found := k.GetDelegation(ctx, zone, redelegateMsg.DelegatorAddress, redelegateMsg.ValidatorSrcAddress) if !found { k.Logger(ctx).Error("unable to find delegation record", "chain", zone.ChainId, "source", redelegateMsg.ValidatorSrcAddress, "dst", redelegateMsg.ValidatorDstAddress, "epoch_number", epochNumber) return fmt.Errorf("unable to find delegation record for chain %s, src: %s, dst: %s, at epoch %d", zone.ChainId, redelegateMsg.ValidatorSrcAddress, redelegateMsg.ValidatorDstAddress, epochNumber) } - srcDelegation.Amount = tgtDelegation.Amount.Sub(redelegateMsg.Amount) + srcDelegation.Amount = srcDelegation.Amount.Sub(redelegateMsg.Amount) + k.SetDelegation(ctx, zone, srcDelegation) valAddr, err = addressutils.ValAddressFromBech32(redelegateMsg.ValidatorDstAddress, zone.AccountPrefix+"valoper") @@ -681,10 +635,6 @@ func (k *Keeper) HandleFailedBeginRedelegate(ctx sdk.Context, msg sdk.Msg, memo } func (k *Keeper) HandleUndelegate(ctx sdk.Context, msg sdk.Msg, completion time.Time, memo string) error { - if completion.IsZero() { - return errors.New("invalid zero nil completion time") - } - k.Logger(ctx).Info("Received MsgUndelegate acknowledgement") // first, type assertion. we should have stakingtypes.MsgUndelegate undelegateMsg, ok := msg.(*stakingtypes.MsgUndelegate) diff --git a/x/interchainstaking/keeper/ibc_packet_handlers_test.go b/x/interchainstaking/keeper/ibc_packet_handlers_test.go index 9e4b3b0ad..fdd0dd68f 100644 --- a/x/interchainstaking/keeper/ibc_packet_handlers_test.go +++ b/x/interchainstaking/keeper/ibc_packet_handlers_test.go @@ -1,12 +1,11 @@ package keeper_test import ( - "crypto/sha256" "fmt" "testing" "time" - sdkmath "cosmossdk.io/math" + "cosmossdk.io/math" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -20,6 +19,7 @@ import ( "github.com/ingenuity-build/quicksilver/app" "github.com/ingenuity-build/quicksilver/utils/addressutils" + "github.com/ingenuity-build/quicksilver/utils/randomutils" icstypes "github.com/ingenuity-build/quicksilver/x/interchainstaking/types" ) @@ -725,7 +725,7 @@ func (suite *KeeperTestSuite) TestReceiveAckErrForBeginRedelegate() { packetData := icatypes.InterchainAccountPacketData{ Type: icatypes.EXECUTE_TX, Data: data, - Memo: fmt.Sprintf("rebalance/%d", 1), + Memo: icstypes.EpochRebalanceMemo(1), } packet := channeltypes.Packet{Data: quicksilver.InterchainstakingKeeper.GetCodec().MustMarshalJSON(&packetData)} @@ -744,11 +744,11 @@ func (suite *KeeperTestSuite) TestReceiveAckErrForBeginRedelegate() { } func (suite *KeeperTestSuite) TestReceiveAckErrForBeginUndelegate() { - hash1 := fmt.Sprintf("%x", sha256.Sum256([]byte{0x01})) - hash2 := fmt.Sprintf("%x", sha256.Sum256([]byte{0x02})) - hash3 := fmt.Sprintf("%x", sha256.Sum256([]byte{0x03})) - delegator1 := addressutils.GenerateAccAddressForTest().String() - delegator2 := addressutils.GenerateAccAddressForTest().String() + hash1 := randomutils.GenerateRandomHashAsHex(32) + hash2 := randomutils.GenerateRandomHashAsHex(32) + hash3 := randomutils.GenerateRandomHashAsHex(32) + delegator1 := addressutils.GenerateAddressForTestWithPrefix("quick") + delegator2 := addressutils.GenerateAddressForTestWithPrefix("quick") tests := []struct { name string @@ -1063,128 +1063,6 @@ func (suite *KeeperTestSuite) TestReceiveAckErrForBeginUndelegate() { } }, }, - // TODO: fix this test - // { - // name: "2 wdr, random_rr, 1 vals, 1k; 2 vals; 123 + 456 ", - // epoch: 1, - // withdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { - // vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) - // return []icstypes.WithdrawalRecord{ - // { - // ChainId: s.chainB.ChainID, - // Delegator: delegator1, - // Distribution: []*icstypes.Distribution{ - // { - // Valoper: vals[0], - // Amount: 1000, - // }, - // }, - // Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), - // Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), - // BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewDec(1000).Quo(sdk.MustNewDecFromStr(fmt.Sprintf("%f", randRr))).TruncateInt()), - // Txhash: hash1, - // Status: icskeeper.WithdrawStatusUnbond, - // }, - // { - // ChainId: s.chainB.ChainID, - // Delegator: delegator2, - // Distribution: []*icstypes.Distribution{ - // { - // Valoper: vals[1], - // Amount: 123, - // }, - // { - // Valoper: vals[2], - // Amount: 456, - // }, - // }, - // Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), - // Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(579))), - // BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewDec(579).Quo(sdk.MustNewDecFromStr(fmt.Sprintf("%f", randRr))).TruncateInt()), - // Txhash: hash2, - // Status: icskeeper.WithdrawStatusUnbond, - // }, - // } - // }, - // unbondingRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.UnbondingRecord { - // vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) - // return []icstypes.UnbondingRecord{ - // { - // ChainId: s.chainB.ChainID, - // EpochNumber: 1, - // Validator: vals[0], - // RelatedTxhash: []string{hash1}, - // }, - // { - // ChainId: s.chainB.ChainID, - // EpochNumber: 1, - // Validator: vals[1], - // RelatedTxhash: []string{hash2}, - // }, - // // { - // // ChainID: s.chainB.ChainID, - // // EpochNumber: 1, - // // Validator: vals[2], - // // RelatedTxhash: []string{hash2}, - // // }, - // } - // }, - // msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { - // vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) - // return []sdk.Msg{ - // &stakingtypes.MsgUndelegate{ - // DelegatorAddress: zone.DelegationAddress.Address, - // ValidatorAddress: vals[0], - // Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000)), - // }, - // &stakingtypes.MsgUndelegate{ - // DelegatorAddress: zone.DelegationAddress.Address, - // ValidatorAddress: vals[1], - // Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(123)), - // }, - // } - // }, - // expectedWithdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { - // vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) - // return []icstypes.WithdrawalRecord{ - // { - // ChainId: s.chainB.ChainID, - // Delegator: delegator1, - // Distribution: nil, - // Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), - // Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), - // BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewDec(1000).Quo(sdk.MustNewDecFromStr(fmt.Sprintf("%f", randRr))).TruncateInt()), - // Txhash: hash1, - // Status: icskeeper.WithdrawStatusQueued, - // }, - // { - // ChainId: s.chainB.ChainID, - // Delegator: delegator2, - // Distribution: nil, - // Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), - // Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(123))), - // BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewDec(123).Quo(sdk.MustNewDecFromStr(fmt.Sprintf("%f", randRr))).TruncateInt()), - // Txhash: fmt.Sprintf("%064d", 1), - // Status: icskeeper.WithdrawStatusQueued, - // }, - // { - // ChainId: s.chainB.ChainID, - // Delegator: delegator2, - // Distribution: []*icstypes.Distribution{ - // { - // Valoper: vals[2], - // Amount: 456, - // }, - // }, - // Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), - // Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(456))), - // BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewDec(456).Quo(sdk.MustNewDecFromStr(fmt.Sprintf("%f", randRr))).TruncateInt()), - // Txhash: hash2, - // Status: icskeeper.WithdrawStatusUnbond, - // }, - // } - // }, - // }, } for _, test := range tests { @@ -1215,7 +1093,7 @@ func (suite *KeeperTestSuite) TestReceiveAckErrForBeginUndelegate() { packetData := icatypes.InterchainAccountPacketData{ Type: icatypes.EXECUTE_TX, Data: data, - Memo: fmt.Sprintf("withdrawal/%d", test.epoch), + Memo: icstypes.EpochWithdrawalMemo(test.epoch), } packet := channeltypes.Packet{Data: quicksilver.InterchainstakingKeeper.GetCodec().MustMarshalJSON(&packetData)} @@ -1339,9 +1217,9 @@ func (suite *KeeperTestSuite) TestRebalanceDueToIntentChange() { DelegatorAddress: zone.DelegationAddress.Address, ValidatorSrcAddress: record.Source, ValidatorDstAddress: record.Destination, - Amount: sdk.NewCoin("uatom", sdkmath.NewInt(record.Amount)), + Amount: sdk.NewCoin("uatom", math.NewInt(record.Amount)), } - err := quicksilver.InterchainstakingKeeper.HandleBeginRedelegate(ctx, &msg, time.Now().Add(time.Hour*24*7), fmt.Sprintf("rebalance/%d", 2)) + err := quicksilver.InterchainstakingKeeper.HandleBeginRedelegate(ctx, &msg, time.Now().Add(time.Hour*24*7), icstypes.EpochRebalanceMemo(2)) if err != nil { return false } @@ -1467,9 +1345,9 @@ func (suite *KeeperTestSuite) TestRebalanceDueToDelegationChange() { DelegatorAddress: zone.DelegationAddress.Address, ValidatorSrcAddress: record.Source, ValidatorDstAddress: record.Destination, - Amount: sdk.NewCoin("uatom", sdkmath.NewInt(record.Amount)), + Amount: sdk.NewCoin("uatom", math.NewInt(record.Amount)), } - err := quicksilver.InterchainstakingKeeper.HandleBeginRedelegate(ctx, &msg, time.Now().Add(time.Hour*24*7), fmt.Sprintf("rebalance/%d", 2)) + err := quicksilver.InterchainstakingKeeper.HandleBeginRedelegate(ctx, &msg, time.Now().Add(time.Hour*24*7), icstypes.EpochRebalanceMemo(2)) if err != nil { return false } @@ -1750,3 +1628,651 @@ func (suite *KeeperTestSuite) Test_v046Callback() { }) } } + +func (suite *KeeperTestSuite) TestReceiveAckForBeginUndelegate() { + hash1 := randomutils.GenerateRandomHashAsHex(32) + hash2 := randomutils.GenerateRandomHashAsHex(32) + hash3 := randomutils.GenerateRandomHashAsHex(32) + delegator1 := addressutils.GenerateAddressForTestWithPrefix("quick") + delegator2 := addressutils.GenerateAddressForTestWithPrefix("quick") + oneMonth := time.Now().AddDate(0, 1, 0).UTC() + nilTime := time.Time{} + + tests := []struct { + name string + epoch int64 + withdrawalRecords func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord + unbondingRecords func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.UnbondingRecord + msgs func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg + completionTime time.Time + expectedWithdrawalRecords func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord + }{ + { + name: "1 wdr, 2 vals, 1k+1k, 1800 qasset", + epoch: 1, + withdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + { + Valoper: vals[1], + Amount: 1000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(2000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(1800)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + }, + } + }, + unbondingRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.UnbondingRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.UnbondingRecord{ + { + ChainId: suite.chainB.ChainID, + EpochNumber: 1, + Validator: vals[0], + RelatedTxhash: []string{hash1}, + }, + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &stakingtypes.MsgUndelegate{ + DelegatorAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000)), + }, + } + }, + completionTime: oneMonth, + expectedWithdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + { + Valoper: vals[1], + Amount: 1000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(2000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(1800)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + CompletionTime: oneMonth, + }, + } + }, + }, + { + name: "1 wdr, 1 vals, 1k, 900 qasset", + epoch: 1, + withdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(900)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + }, + } + }, + unbondingRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.UnbondingRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.UnbondingRecord{ + { + ChainId: suite.chainB.ChainID, + EpochNumber: 1, + Validator: vals[0], + RelatedTxhash: []string{hash1}, + }, + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &stakingtypes.MsgUndelegate{ + DelegatorAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000)), + }, + } + }, + completionTime: oneMonth, + expectedWithdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(900)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + CompletionTime: oneMonth, + }, + } + }, + }, + { + name: "3 wdr, 2 vals, 1k+0.5k, 1350 qasset; 1k+2k, 2700 qasset; 600+400, 900qasset", + epoch: 2, + withdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + { + Valoper: vals[1], + Amount: 500, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1500))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(1350)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + }, + { + ChainId: suite.chainB.ChainID, + Delegator: delegator2, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + { + Valoper: vals[1], + Amount: 2000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(3000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(2700)), + Txhash: hash2, + Status: icstypes.WithdrawStatusUnbond, + }, + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 600, + }, + { + Valoper: vals[1], + Amount: 400, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(900)), + Txhash: hash3, + Status: icstypes.WithdrawStatusUnbond, + }, + } + }, + unbondingRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.UnbondingRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.UnbondingRecord{ + { + ChainId: suite.chainB.ChainID, + EpochNumber: 2, + Validator: vals[1], + RelatedTxhash: []string{hash1, hash2, hash3}, + }, + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &stakingtypes.MsgUndelegate{ + DelegatorAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[1], + Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(2900)), + }, + } + }, + completionTime: nilTime, + expectedWithdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + { + Valoper: vals[1], + Amount: 500, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1500))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(1350)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + }, + { + ChainId: suite.chainB.ChainID, + Delegator: delegator2, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + { + Valoper: vals[1], + Amount: 2000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(3000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(2700)), + Txhash: hash2, + Status: icstypes.WithdrawStatusUnbond, + }, + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 600, + }, + { + Valoper: vals[1], + Amount: 400, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(900)), + Txhash: hash3, + Status: icstypes.WithdrawStatusUnbond, + }, + } + }, + }, + { + name: "2 wdr, 1 vals, 1k; 2 vals; 123 + 456 ", + epoch: 1, + withdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.WithdrawalRecord{ + { + ChainId: suite.chainB.ChainID, + Delegator: delegator1, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[0], + Amount: 1000, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(900)), + Txhash: hash1, + Status: icstypes.WithdrawStatusUnbond, + }, + { + ChainId: suite.chainB.ChainID, + Delegator: delegator2, + Distribution: []*icstypes.Distribution{ + { + Valoper: vals[1], + Amount: 123, + }, + { + Valoper: vals[2], + Amount: 456, + }, + }, + Recipient: addressutils.GenerateAddressForTestWithPrefix(zone.GetAccountPrefix()), + Amount: sdk.NewCoins(sdk.NewCoin(zone.BaseDenom, sdk.NewInt(579))), + BurnAmount: sdk.NewCoin(zone.LocalDenom, sdk.NewInt(521)), + Txhash: hash2, + Status: icstypes.WithdrawStatusUnbond, + }, + } + }, + unbondingRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.UnbondingRecord { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []icstypes.UnbondingRecord{ + { + ChainId: suite.chainB.ChainID, + EpochNumber: 1, + Validator: vals[0], + RelatedTxhash: []string{hash1}, + }, + { + ChainId: suite.chainB.ChainID, + EpochNumber: 1, + Validator: vals[1], + RelatedTxhash: []string{hash2}, + }, + } + }, + msgs: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []sdk.Msg { + vals := qs.InterchainstakingKeeper.GetValidatorAddresses(ctx, zone.ChainId) + return []sdk.Msg{ + &stakingtypes.MsgUndelegate{ + DelegatorAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[0], + Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000)), + }, + &stakingtypes.MsgUndelegate{ + DelegatorAddress: zone.DelegationAddress.Address, + ValidatorAddress: vals[1], + Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(123)), + }, + } + }, + completionTime: nilTime, + expectedWithdrawalRecords: func(ctx sdk.Context, qs *app.Quicksilver, zone icstypes.Zone) []icstypes.WithdrawalRecord { + return []icstypes.WithdrawalRecord{} + }, + }, + } + + for _, test := range tests { + suite.Run(test.name, func() { + suite.SetupTest() + suite.setupTestZones() + + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + if !found { + suite.Fail("unable to retrieve zone for test") + } + + for _, wdr := range test.withdrawalRecords(ctx, quicksilver, zone) { + quicksilver.InterchainstakingKeeper.SetWithdrawalRecord(ctx, wdr) + } + + for _, ubr := range test.unbondingRecords(ctx, quicksilver, zone) { + quicksilver.InterchainstakingKeeper.SetUnbondingRecord(ctx, ubr) + } + + msgs := test.msgs(ctx, quicksilver, zone) + data, err := icatypes.SerializeCosmosTx(quicksilver.InterchainstakingKeeper.GetCodec(), msgs) + suite.Require().NoError(err) + + // validate memo < 256 bytes + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: data, + Memo: icstypes.EpochWithdrawalMemo(test.epoch), + } + + packet := channeltypes.Packet{Data: quicksilver.InterchainstakingKeeper.GetCodec().MustMarshalJSON(&packetData)} + + responses := make([]*codectypes.Any, 0) + + for range msgs { + response := stakingtypes.MsgUndelegateResponse{ + CompletionTime: test.completionTime, + } + + anyResponse, err := codectypes.NewAnyWithValue(&response) + suite.Require().NoError(err) + responses = append(responses, anyResponse) + } + + txMsgData := &sdk.TxMsgData{ + MsgResponses: responses, + } + + ackData := icatypes.ModuleCdc.MustMarshal(txMsgData) + + acknowledgement := channeltypes.NewResultAcknowledgement(ackData) + ackBytes, err := icatypes.ModuleCdc.MarshalJSON(&acknowledgement) + suite.Require().NoError(err) + + // call handler + + for _, ubr := range test.unbondingRecords(ctx, quicksilver, zone) { + _, found = quicksilver.InterchainstakingKeeper.GetUnbondingRecord(ctx, zone.ChainId, ubr.Validator, test.epoch) + suite.Require().True(found) + } + + err = quicksilver.InterchainstakingKeeper.HandleAcknowledgement(ctx, packet, ackBytes) + suite.Require().NoError(err) + + for idx, ewdr := range test.expectedWithdrawalRecords(ctx, quicksilver, zone) { + wdr, found := quicksilver.InterchainstakingKeeper.GetWithdrawalRecord(ctx, zone.ChainId, ewdr.Txhash, ewdr.Status) + suite.Require().True(found) + suite.Require().Equal(ewdr.Amount, wdr.Amount) + suite.Require().Equal(ewdr.BurnAmount, wdr.BurnAmount) + suite.Require().Equal(ewdr.Delegator, wdr.Delegator) + suite.Require().Equal(ewdr.Distribution, wdr.Distribution, idx) + suite.Require().Equal(ewdr.Status, wdr.Status) + suite.Require().Equal(ewdr.CompletionTime, wdr.CompletionTime) + } + }) + } +} + +func (suite *KeeperTestSuite) TestReceiveAckForBeginRedelegateNonNilCompletion() { + suite.SetupTest() + suite.setupTestZones() + + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + complete := time.Now().UTC().AddDate(0, 0, 21) + + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + if !found { + suite.Fail("unable to retrieve zone for test") + } + validators := quicksilver.InterchainstakingKeeper.GetValidators(ctx, zone.ChainId) + // create redelegation record + record := icstypes.RedelegationRecord{ + ChainId: suite.chainB.ChainID, + EpochNumber: 1, + Source: validators[0].ValoperAddress, + Destination: validators[1].ValoperAddress, + Amount: 1000, + } + + quicksilver.InterchainstakingKeeper.SetRedelegationRecord(ctx, record) + + beforeSource := icstypes.Delegation{ + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: validators[0].ValoperAddress, + Amount: sdk.NewCoin(zone.BaseDenom, math.NewInt(2000)), + } + + quicksilver.InterchainstakingKeeper.SetDelegation(ctx, &zone, beforeSource) + + redelegate := &stakingtypes.MsgBeginRedelegate{DelegatorAddress: zone.DelegationAddress.Address, ValidatorSrcAddress: validators[0].ValoperAddress, ValidatorDstAddress: validators[1].ValoperAddress, Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))} + data, err := icatypes.SerializeCosmosTx(quicksilver.InterchainstakingKeeper.GetCodec(), []sdk.Msg{redelegate}) + suite.Require().NoError(err) + + // validate memo < 256 bytes + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: data, + Memo: icstypes.EpochRebalanceMemo(1), + } + + packet := channeltypes.Packet{Data: quicksilver.InterchainstakingKeeper.GetCodec().MustMarshalJSON(&packetData)} + + response := stakingtypes.MsgUndelegateResponse{ + CompletionTime: complete, + } + + anyResponse, err := codectypes.NewAnyWithValue(&response) + suite.Require().NoError(err) + + txMsgData := &sdk.TxMsgData{ + MsgResponses: []*codectypes.Any{anyResponse}, + } + + ackData := icatypes.ModuleCdc.MustMarshal(txMsgData) + + acknowledgement := channeltypes.NewResultAcknowledgement(ackData) + ackBytes, err := icatypes.ModuleCdc.MarshalJSON(&acknowledgement) + suite.Require().NoError(err) + + // call handler + + _, found = quicksilver.InterchainstakingKeeper.GetRedelegationRecord(ctx, zone.ChainId, validators[0].ValoperAddress, validators[1].ValoperAddress, 1) + suite.Require().True(found) + + err = quicksilver.InterchainstakingKeeper.HandleAcknowledgement(ctx, packet, ackBytes) + suite.Require().NoError(err) + + afterRedelegation, found := quicksilver.InterchainstakingKeeper.GetRedelegationRecord(ctx, zone.ChainId, validators[0].ValoperAddress, validators[1].ValoperAddress, 1) + suite.Require().True(found) + suite.Require().Equal(complete, afterRedelegation.CompletionTime) + + afterSource, found := quicksilver.InterchainstakingKeeper.GetDelegation(ctx, &zone, zone.DelegationAddress.Address, validators[1].ValoperAddress) + suite.Require().True(found) + suite.Require().Equal(beforeSource.Amount.Sub(redelegate.Amount), afterSource.Amount) + + afterTarget, found := quicksilver.InterchainstakingKeeper.GetDelegation(ctx, &zone, zone.DelegationAddress.Address, validators[1].ValoperAddress) + suite.Require().True(found) + suite.Require().Equal(complete.Unix(), afterTarget.RedelegationEnd) + /// target did not exist before redelegation + suite.Require().Equal(redelegate.Amount, afterTarget.Amount) +} + +func (suite *KeeperTestSuite) TestReceiveAckForBeginRedelegateNilCompletion() { + suite.SetupTest() + suite.setupTestZones() + + epoch := int64(2) + + quicksilver := suite.GetQuicksilverApp(suite.chainA) + ctx := suite.chainA.GetContext() + complete := time.Time{} + + zone, found := quicksilver.InterchainstakingKeeper.GetZone(ctx, suite.chainB.ChainID) + if !found { + suite.Fail("unable to retrieve zone for test") + } + validators := quicksilver.InterchainstakingKeeper.GetValidators(ctx, zone.ChainId) + // create redelegation record + record := icstypes.RedelegationRecord{ + ChainId: suite.chainB.ChainID, + EpochNumber: epoch, + Source: validators[0].ValoperAddress, + Destination: validators[1].ValoperAddress, + Amount: 1000, + } + + quicksilver.InterchainstakingKeeper.SetRedelegationRecord(ctx, record) + + beforeTarget := icstypes.Delegation{ + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: validators[1].ValoperAddress, + Amount: sdk.NewCoin(zone.BaseDenom, math.NewInt(2000)), + } + + beforeSource := icstypes.Delegation{ + DelegationAddress: zone.DelegationAddress.Address, + ValidatorAddress: validators[0].ValoperAddress, + Amount: sdk.NewCoin(zone.BaseDenom, math.NewInt(1001)), + } + + quicksilver.InterchainstakingKeeper.SetDelegation(ctx, &zone, beforeTarget) + quicksilver.InterchainstakingKeeper.SetDelegation(ctx, &zone, beforeSource) + + redelegate := &stakingtypes.MsgBeginRedelegate{DelegatorAddress: zone.DelegationAddress.Address, ValidatorSrcAddress: validators[0].ValoperAddress, ValidatorDstAddress: validators[1].ValoperAddress, Amount: sdk.NewCoin(zone.BaseDenom, sdk.NewInt(1000))} + data, err := icatypes.SerializeCosmosTx(quicksilver.InterchainstakingKeeper.GetCodec(), []sdk.Msg{redelegate}) + suite.Require().NoError(err) + + // validate memo < 256 bytes + packetData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: data, + Memo: icstypes.EpochRebalanceMemo(epoch), + } + + packet := channeltypes.Packet{Data: quicksilver.InterchainstakingKeeper.GetCodec().MustMarshalJSON(&packetData)} + + response := stakingtypes.MsgUndelegateResponse{ + CompletionTime: complete, + } + + anyResponse, err := codectypes.NewAnyWithValue(&response) + suite.Require().NoError(err) + + txMsgData := &sdk.TxMsgData{ + MsgResponses: []*codectypes.Any{anyResponse}, + } + + ackData := icatypes.ModuleCdc.MustMarshal(txMsgData) + + acknowledgement := channeltypes.NewResultAcknowledgement(ackData) + ackBytes, err := icatypes.ModuleCdc.MarshalJSON(&acknowledgement) + suite.Require().NoError(err) + + // call handler + + _, found = quicksilver.InterchainstakingKeeper.GetRedelegationRecord(ctx, zone.ChainId, validators[0].ValoperAddress, validators[1].ValoperAddress, epoch) + suite.Require().True(found) + + err = quicksilver.InterchainstakingKeeper.HandleAcknowledgement(ctx, packet, ackBytes) + suite.Require().NoError(err) + + _, found = quicksilver.InterchainstakingKeeper.GetRedelegationRecord(ctx, zone.ChainId, validators[0].ValoperAddress, validators[1].ValoperAddress, epoch) + suite.Require().False(found) // redelegation record should have been removed. + + afterSource, found := quicksilver.InterchainstakingKeeper.GetDelegation(ctx, &zone, zone.DelegationAddress.Address, validators[0].ValoperAddress) + suite.Require().True(found) + suite.Require().Equal(beforeSource.Amount.Sub(redelegate.Amount), afterSource.Amount) + + afterTarget, found := quicksilver.InterchainstakingKeeper.GetDelegation(ctx, &zone, zone.DelegationAddress.Address, validators[1].ValoperAddress) + suite.Require().True(found) + suite.Require().Equal(complete.Unix(), afterTarget.RedelegationEnd) + suite.Require().Equal(beforeTarget.Amount.Add(redelegate.Amount), afterTarget.Amount) +} diff --git a/x/interchainstaking/keeper/keeper.go b/x/interchainstaking/keeper/keeper.go index e4d407d20..2544e8f87 100644 --- a/x/interchainstaking/keeper/keeper.go +++ b/x/interchainstaking/keeper/keeper.go @@ -633,7 +633,7 @@ func (k *Keeper) Rebalance(ctx sdk.Context, zone *types.Zone, epochNumber int64) return nil } k.Logger(ctx).Info("Send rebalancing messages", "msgs", msgs) - return k.SubmitTx(ctx, msgs, zone.DelegationAddress, fmt.Sprintf("%s/%d", types.MsgTypeRebalance, epochNumber), zone.MessagesPerTx) + return k.SubmitTx(ctx, msgs, zone.DelegationAddress, types.EpochRebalanceMemo(epochNumber), zone.MessagesPerTx) } // UnmarshalValidatorsResponse attempts to umarshal a byte slice into a QueryValidatorsResponse. diff --git a/x/interchainstaking/keeper/redemptions.go b/x/interchainstaking/keeper/redemptions.go index a1a23e6c0..6654c1441 100644 --- a/x/interchainstaking/keeper/redemptions.go +++ b/x/interchainstaking/keeper/redemptions.go @@ -242,7 +242,7 @@ WITHDRAWAL: k.Logger(ctx).Info("unbonding messages to send", "msg", msgs) - err = k.SubmitTx(ctx, msgs, zone.DelegationAddress, fmt.Sprintf("%s/%d", types.MsgTypeWithdrawal, epoch), zone.MessagesPerTx) + err = k.SubmitTx(ctx, msgs, zone.DelegationAddress, types.EpochWithdrawalMemo(epoch), zone.MessagesPerTx) if err != nil { return err } diff --git a/x/interchainstaking/types/ibc_packet.go b/x/interchainstaking/types/ibc_packet.go index 07c353cf8..483cebe7a 100644 --- a/x/interchainstaking/types/ibc_packet.go +++ b/x/interchainstaking/types/ibc_packet.go @@ -43,3 +43,19 @@ func ParseTxMsgMemo(memo, msgType string) (txHash string, err error) { return parts[1], err } + +func EpochMsgMemo(msgType string, epoch int64) string { + return fmt.Sprintf("%s/%d", msgType, epoch) +} + +func EpochRebalanceMemo(epoch int64) string { + return EpochMsgMemo(MsgTypeRebalance, epoch) +} + +func EpochWithdrawalMemo(epoch int64) string { + return EpochMsgMemo(MsgTypeWithdrawal, epoch) +} + +func TxUnbondSendMemo(hash string) string { + return fmt.Sprintf("%s/%s", MsgTypeUnbondSend, hash) +} diff --git a/x/interchainstaking/types/ibc_packet_test.go b/x/interchainstaking/types/ibc_packet_test.go index 3b1cba61c..e721ec5c5 100644 --- a/x/interchainstaking/types/ibc_packet_test.go +++ b/x/interchainstaking/types/ibc_packet_test.go @@ -18,14 +18,14 @@ func TestParseMsgMemo(t *testing.T) { }{ { name: "valid rebalance", - memo: types.MsgTypeRebalance + "/" + "10", + memo: types.EpochRebalanceMemo(10), msgType: types.MsgTypeRebalance, wantErr: false, expectedEpochNumber: 10, }, { name: "valid withdrawal", - memo: types.MsgTypeWithdrawal + "/" + "10", + memo: types.EpochWithdrawalMemo(10), msgType: types.MsgTypeWithdrawal, wantErr: false, expectedEpochNumber: 10,