Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

close delegation channel tx #1242

Merged
merged 9 commits into from
Jul 19, 2024
9 changes: 9 additions & 0 deletions proto/stride/stakeibc/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ service Msg {
rpc DeleteValidator(MsgDeleteValidator) returns (MsgDeleteValidatorResponse);
rpc RestoreInterchainAccount(MsgRestoreInterchainAccount)
returns (MsgRestoreInterchainAccountResponse);
rpc CloseDelegationChannel(MsgCloseDelegationChannel)
returns (MsgCloseDelegationChannelResponse);
rpc UpdateValidatorSharesExchRate(MsgUpdateValidatorSharesExchRate)
returns (MsgUpdateValidatorSharesExchRateResponse);
rpc CalibrateDelegation(MsgCalibrateDelegation)
Expand Down Expand Up @@ -193,6 +195,13 @@ message MsgRestoreInterchainAccount {
}
message MsgRestoreInterchainAccountResponse {}

message MsgCloseDelegationChannel {
string creator = 1;
string channel_id = 2;
string port_id = 3;
}
message MsgCloseDelegationChannelResponse {}

message MsgUpdateValidatorSharesExchRate {
string creator = 1;
string chain_id = 2;
Expand Down
38 changes: 38 additions & 0 deletions x/stakeibc/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func GetTxCmd() *cobra.Command {
cmd.AddCommand(CmdChangeMultipleValidatorWeight())
cmd.AddCommand(CmdDeleteValidator())
cmd.AddCommand(CmdRestoreInterchainAccount())
cmd.AddCommand(CmdCloseDelegationChannel())
cmd.AddCommand(CmdUpdateValidatorSharesExchRate())
cmd.AddCommand(CmdCalibrateDelegation())
cmd.AddCommand(CmdClearBalance())
Expand Down Expand Up @@ -537,6 +538,43 @@ ex:
return cmd
}

func CmdCloseDelegationChannel() *cobra.Command {
cmd := &cobra.Command{
Use: "close-delegation-channel [channel-id] [port-id]",
Short: "Broadcast message close-delegation-channel",
Long: strings.TrimSpace(
`Closes a delegation ICA channel. This can only be run by the admin

Ex:
>>> strided tx close-delegation-channel channel-0 icacontroller-cosmoshub-4.DELEGATION
`),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
channelId := args[0]
portId := args[1]

clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

msg := types.NewMsgCloseDelegationChannel(
clientCtx.GetFromAddress().String(),
channelId,
portId,
)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}

func CmdUpdateValidatorSharesExchRate() *cobra.Command {
cmd := &cobra.Command{
Use: "update-delegation [chainid] [valoper]",
Expand Down
15 changes: 15 additions & 0 deletions x/stakeibc/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,21 @@ func (k msgServer) RestoreInterchainAccount(goCtx context.Context, msg *types.Ms
return &types.MsgRestoreInterchainAccountResponse{}, nil
}

// Admin transaction to close an ICA channel
// This can be used if there are records stuck in state IN_PROGRESS after a channel has been re-opened after a timeout
// After the closure, the a new channel can be permissionlessly re-opened with RestoreInterchainAccount
func (k msgServer) CloseDelegationChannel(goCtx context.Context, msg *types.MsgCloseDelegationChannel) (*types.MsgCloseDelegationChannelResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
_, capability, err := k.IBCKeeper.ChannelKeeper.LookupModuleByChannel(ctx, msg.PortId, msg.ChannelId)
if err != nil {
return nil, errorsmod.Wrap(err, "could not retrieve capability from port ID and channel ID")
}
if err := k.IBCKeeper.ChannelKeeper.ChanCloseInit(ctx, msg.PortId, msg.ChannelId, capability); err != nil {
sampocs marked this conversation as resolved.
Show resolved Hide resolved
return nil, errorsmod.Wrapf(err, "unable to initiate channel closure")
}
return &types.MsgCloseDelegationChannelResponse{}, nil
}

// This kicks off two ICQs, each with a callback, that will update the number of tokens on a validator
// after being slashed. The flow is:
// 1. QueryValidatorSharesToTokensRate (ICQ)
Expand Down
38 changes: 38 additions & 0 deletions x/stakeibc/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2585,6 +2585,44 @@ func (s *KeeperTestSuite) TestRestoreInterchainAccount_NoRecordChange_Success()
s.verifyLSMDepositStatus(tc.lsmTokenDepositStatusUpdate, false)
}

// ----------------------------------------------------
// CloseDelegationChannel
// ----------------------------------------------------

func (s *KeeperTestSuite) TestCloseDelegationChannel() {
channelID, portID := s.CreateICAChannel(types.FormatHostZoneICAOwner(HostChainId, types.ICAAccountType_DELEGATION))

// Confirm the channel is open
channel, found := s.App.IBCKeeper.ChannelKeeper.GetChannel(s.Ctx, portID, channelID)
s.Require().True(found, "channel should be found")
s.Require().Equal(channeltypes.OPEN, channel.State, "channel should be open")

// Close the channel
msg := types.MsgCloseDelegationChannel{
ChannelId: channelID,
PortId: portID,
}
_, err := s.GetMsgServer().CloseDelegationChannel(sdk.UnwrapSDKContext(s.Ctx), &msg)
s.Require().NoError(err, "no error expected when closing channel")

// Confirm the channel is closed
channel, found = s.App.IBCKeeper.ChannelKeeper.GetChannel(s.Ctx, portID, channelID)
s.Require().True(found, "channel should be found")
s.Require().Equal(channeltypes.CLOSED, channel.State, "channel should be closed")

// Attempt to call it again, it should error since the channel is already closed
_, err = s.GetMsgServer().CloseDelegationChannel(sdk.UnwrapSDKContext(s.Ctx), &msg)
s.Require().ErrorContains(err, "channel is already CLOSED")

// Attempt to call it with a different channel ID, it should error since the channel wont be found
invalidMsg := types.MsgCloseDelegationChannel{
ChannelId: "channel-10",
PortId: portID,
}
_, err = s.GetMsgServer().CloseDelegationChannel(sdk.UnwrapSDKContext(s.Ctx), &invalidMsg)
s.Require().ErrorContains(err, "not found")
}

// ----------------------------------------------------
// UpdateInnerRedemptionRateBounds
// ----------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions x/stakeibc/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func RegisterCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&AddValidatorsProposal{}, "stakeibc/AddValidatorsProposal", nil)
cdc.RegisterConcrete(&ToggleLSMProposal{}, "stakeibc/ToggleLSMProposal", nil)
cdc.RegisterConcrete(&MsgRestoreInterchainAccount{}, "stakeibc/RestoreInterchainAccount", nil)
cdc.RegisterConcrete(&MsgCloseDelegationChannel{}, "stakeibc/CloseDelegationChannel", nil)
sampocs marked this conversation as resolved.
Show resolved Hide resolved
cdc.RegisterConcrete(&MsgUpdateValidatorSharesExchRate{}, "stakeibc/UpdateValidatorSharesExchRate", nil)
cdc.RegisterConcrete(&MsgCalibrateDelegation{}, "stakeibc/CalibrateDelegation", nil)
cdc.RegisterConcrete(&MsgUpdateInnerRedemptionRateBounds{}, "stakeibc/UpdateInnerRedemptionRateBounds", nil)
Expand All @@ -43,6 +44,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
&MsgChangeValidatorWeights{},
&MsgDeleteValidator{},
&MsgRestoreInterchainAccount{},
&MsgCloseDelegationChannel{},
&MsgUpdateValidatorSharesExchRate{},
&MsgCalibrateDelegation{},
&MsgUpdateInnerRedemptionRateBounds{},
Expand Down
79 changes: 79 additions & 0 deletions x/stakeibc/types/message_close_delegation_channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package types

import (
"strings"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"
icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types"

"github.com/Stride-Labs/stride/v22/utils"
"github.com/Stride-Labs/stride/v22/x/stakeibc/migrations/v2/types"
)

const TypeMsgCloseDelegationChannel = "close_delegation_channel"

var (
_ sdk.Msg = &MsgCloseDelegationChannel{}
_ legacytx.LegacyMsg = &MsgCloseDelegationChannel{}
)

func NewMsgCloseDelegationChannel(creator, channelId, portId string) *MsgCloseDelegationChannel {
return &MsgCloseDelegationChannel{
Creator: creator,
ChannelId: channelId,
PortId: portId,
}
}

func (msg *MsgCloseDelegationChannel) Route() string {
return RouterKey
}

func (msg *MsgCloseDelegationChannel) Type() string {
return TypeMsgCloseDelegationChannel
}

func (msg *MsgCloseDelegationChannel) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}

func (msg *MsgCloseDelegationChannel) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}

func (msg *MsgCloseDelegationChannel) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
if err := utils.ValidateAdminAddress(msg.Creator); err != nil {
return err
}

if msg.ChannelId == "" {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "channel ID must be specified")
}
if msg.PortId == "" {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "port ID must be specified")
}

if err := ValidateChannelId(msg.ChannelId); err != nil {
return err
}
if !strings.HasPrefix(msg.PortId, icatypes.ControllerPortPrefix) {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "channel must be an ICA channel")
}
if !strings.HasSuffix(msg.PortId, types.ICAAccountType_DELEGATION.String()) {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "channel must be the delegation ICA channel")
}

return nil
}
123 changes: 123 additions & 0 deletions x/stakeibc/types/message_close_delegation_channel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package types_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/Stride-Labs/stride/v22/app/apptesting"
"github.com/Stride-Labs/stride/v22/x/stakeibc/types"
)

func TestMsgCloseDelegationChannel(t *testing.T) {
validNotAdminAddress, invalidAddress := apptesting.GenerateTestAddrs()
validAdminAddress, ok := apptesting.GetAdminAddress()
require.True(t, ok)

validChannelId := "channel-10"
validPortId := "icacontroller-DELEGATION"

tests := []struct {
name string
msg types.MsgCloseDelegationChannel
err string
}{
{
name: "successful message",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: validChannelId,
PortId: validPortId,
},
},
{
name: "invalid creator address",
msg: types.MsgCloseDelegationChannel{
Creator: invalidAddress,
ChannelId: validChannelId,
PortId: validPortId,
},
err: "invalid creator address",
},
{
name: "invalid admin address",
msg: types.MsgCloseDelegationChannel{
Creator: validNotAdminAddress,
ChannelId: validChannelId,
PortId: validPortId,
},
err: "is not an admin",
},
{
name: "invalid channel prefix",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: "chann-1",
PortId: validPortId,
},
err: "invalid channel-id",
},
{
name: "invalid connection suffix",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: "channel-X",
PortId: validPortId,
},
err: "invalid channel-id",
},
{
name: "invalid port ID",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: validChannelId,
PortId: "",
},
err: "port ID must be specified",
},
{
name: "invalid port ID",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: validChannelId,
PortId: "",
},
err: "port ID must be specified",
},
{
name: "not ICA channel",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: validChannelId,
PortId: "DELEGATION",
},
err: "must be an ICA channel",
},
{
name: "not delegation channel",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChannelId: validChannelId,
PortId: "icacontroller-WITHDRAWAL",
},
err: "must be the delegation ICA channel",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.err == "" {
require.NoError(t, test.msg.ValidateBasic(), "test: %v", test.name)

signers := test.msg.GetSigners()
require.Equal(t, len(signers), 1)
require.Equal(t, signers[0].String(), validAdminAddress)

require.Equal(t, test.msg.ChannelId, validChannelId, "channel-id")
require.Equal(t, test.msg.Type(), "close_delegation_channel", "type")
} else {
require.ErrorContains(t, test.msg.ValidateBasic(), test.err, "test: %v", test.name)
}
})
}
}
Loading
Loading