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
Merged
8 changes: 8 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,12 @@ message MsgRestoreInterchainAccount {
}
message MsgRestoreInterchainAccountResponse {}

message MsgCloseDelegationChannel {
string creator = 1;
string chain_id = 2;
}
message MsgCloseDelegationChannelResponse {}

message MsgUpdateValidatorSharesExchRate {
string creator = 1;
string chain_id = 2;
Expand Down
36 changes: 36 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,41 @@ ex:
return cmd
}

func CmdCloseDelegationChannel() *cobra.Command {
cmd := &cobra.Command{
Use: "close-delegation-channel [chain-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 cosmoshub-4
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
chainId := args[0]

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

msg := types.NewMsgCloseDelegationChannel(
clientCtx.GetFromAddress().String(),
chainId,
)
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
30 changes: 30 additions & 0 deletions x/stakeibc/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
proto "github.com/cosmos/gogoproto/proto"
icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types"
Expand Down Expand Up @@ -1001,6 +1002,35 @@ func (k msgServer) RestoreInterchainAccount(goCtx context.Context, msg *types.Ms
return &types.MsgRestoreInterchainAccountResponse{}, nil
}

// Admin transaction to close an ICA channel by sending an ICA with a 1 nanosecond timeout (which will force a timeout and closure)
// 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)

hostZone, found := k.GetHostZone(ctx, msg.ChainId)
if !found {
return nil, types.ErrHostZoneNotFound.Wrapf("chain id %s", msg.ChainId)
}

// Submit an ICA bank send from the delegation ICA account to itself for just 1utoken
delegationIcaOwner := types.FormatHostZoneICAOwner(msg.ChainId, types.ICAAccountType_DELEGATION)
msgSend := []proto.Message{&banktypes.MsgSend{
FromAddress: hostZone.DelegationIcaAddress,
ToAddress: hostZone.DelegationIcaAddress,
Amount: sdk.NewCoins(sdk.NewCoin(hostZone.HostDenom, sdkmath.OneInt())),
}}

// Timeout the ICA 1 nanosecond after the current block time (so it's impossible to be relayed)
timeoutTimestamp := uint64(ctx.BlockTime().UnixNano() + 1)
err := k.SubmitICATxWithoutCallback(ctx, hostZone.ConnectionId, delegationIcaOwner, msgSend, timeoutTimestamp)
if err != nil {
return nil, err
}

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
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
61 changes: 61 additions & 0 deletions x/stakeibc/types/message_close_delegation_channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package types

import (
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"

"github.com/Stride-Labs/stride/v22/utils"
)

const TypeMsgCloseDelegationChannel = "close_delegation_channel"

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

func NewMsgCloseDelegationChannel(creator, chainId string) *MsgCloseDelegationChannel {
return &MsgCloseDelegationChannel{
Creator: creator,
ChainId: chainId,
}
}

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.ChainId == "" {
return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "chain ID must be specified")
}

return nil
}
71 changes: 71 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,71 @@
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)

validChainId := "chain-0"

tests := []struct {
name string
msg types.MsgCloseDelegationChannel
err string
}{
{
name: "successful message",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChainId: validChainId,
},
},
{
name: "invalid creator address",
msg: types.MsgCloseDelegationChannel{
Creator: invalidAddress,
ChainId: validChainId,
},
err: "invalid creator address",
},
{
name: "invalid admin address",
msg: types.MsgCloseDelegationChannel{
Creator: validNotAdminAddress,
ChainId: validChainId,
},
err: "is not an admin",
},
{
name: "invalid chain-id",
msg: types.MsgCloseDelegationChannel{
Creator: validAdminAddress,
ChainId: "",
},
err: "chain ID must be specified",
},
}

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.Type(), "close_delegation_channel", "type")
} else {
require.ErrorContains(t, test.msg.ValidateBasic(), test.err, "test: %v", test.name)
}
})
}
}
Loading
Loading