diff --git a/app/upgrades/v17/upgrades.go b/app/upgrades/v17/upgrades.go index de4f31ad95..6143380d14 100644 --- a/app/upgrades/v17/upgrades.go +++ b/app/upgrades/v17/upgrades.go @@ -2,6 +2,7 @@ package v17 import ( "fmt" + "time" errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" @@ -9,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/cosmos/gogoproto/proto" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" connectiontypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" @@ -32,6 +34,9 @@ var ( RedemptionRateOuterMinAdjustment = sdk.MustNewDecFromStr("0.05") RedemptionRateOuterMaxAdjustment = sdk.MustNewDecFromStr("0.10") + // Define the hub chainId for disabling tokenization + GaiaChainId = "cosmoshub-4" + // Osmosis will have a slighly larger buffer with the redemption rate // since their yield is less predictable OsmosisChainId = "osmosis-1" @@ -111,6 +116,9 @@ func CreateUpgradeHandler( return vm, errorsmod.Wrapf(err, "unable to add rate limits to Osmosis") } + ctx.Logger().Info("Disabling tokenization on the hub...") + DisableTokenization(ctx, stakeibcKeeper, GaiaChainId) + ctx.Logger().Info("Executing Prop 225, SHD Liquidity") if err := ExecuteProp225(ctx, bankKeeper); err != nil { return vm, errorsmod.Wrapf(err, "unable to execute prop 225") @@ -318,6 +326,31 @@ func AddRateLimitToOsmosis(ctx sdk.Context, k ratelimitkeeper.Keeper) error { return nil } +// Sends the ICA message which disables LSM style tokenization of shares from the delegation +// account for this chain as a security to prevent possibility of large/fast withdrawls +func DisableTokenization(ctx sdk.Context, k stakeibckeeper.Keeper, chainId string) error { + hostZone, found := k.GetHostZone(ctx, chainId) + if !found { + return errorsmod.Wrapf(stakeibctypes.ErrHostZoneNotFound, "Unable to find chainId %s to remove tokenization", chainId) + } + + // Build the msg for the disable tokenization ICA tx + var msgs []proto.Message + msgs = append(msgs, &stakeibctypes.MsgDisableTokenizeShares{ + DelegatorAddress: hostZone.DelegationIcaAddress, + }) + + // Send the ICA tx to disable tokenization + timeoutTimestamp := uint64(ctx.BlockTime().Add(24 * time.Hour).UnixNano()) + delegationOwner := stakeibctypes.FormatHostZoneICAOwner(hostZone.ChainId, stakeibctypes.ICAAccountType_DELEGATION) + err := k.SubmitICATxWithoutCallback(ctx, hostZone.ConnectionId, delegationOwner, msgs, timeoutTimestamp) + if err != nil { + return errorsmod.Wrapf(err, "Failed to submit ICA tx to disable tokenization, Messages: %+v", msgs) + } + + return nil +} + // Execute Prop 225, release STRD to stride1auhjs4zgp3ahvrpkspf088r2psz7wpyrypcnal func ExecuteProp225(ctx sdk.Context, k bankkeeper.Keeper) error { communityPoolGrowthAddress := sdk.MustAccAddressFromBech32(CommunityPoolGrowthAddress) diff --git a/app/upgrades/v17/upgrades_test.go b/app/upgrades/v17/upgrades_test.go index c8cf11579c..325a79feda 100644 --- a/app/upgrades/v17/upgrades_test.go +++ b/app/upgrades/v17/upgrades_test.go @@ -7,6 +7,7 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + ibctesting "github.com/cosmos/ibc-go/v7/testing" "github.com/stretchr/testify/suite" icqtypes "github.com/Stride-Labs/stride/v16/x/interchainquery/types" @@ -20,7 +21,6 @@ import ( ) const ( - GaiaChainId = "cosmoshub-4" SommelierChainId = "sommelier-3" Atom = "uatom" @@ -69,11 +69,16 @@ func TestKeeperTestSuite(t *testing.T) { func (s *UpgradeTestSuite) TestUpgrade() { dummyUpgradeHeight := int64(5) + // Create the gaia delegation channel + owner := stakeibctypes.FormatHostZoneICAOwner(v17.GaiaChainId, stakeibctypes.ICAAccountType_DELEGATION) + channelId, portId := s.CreateICAChannel(owner) + // Setup store before upgrade checkHostZonesAfterUpgrade := s.SetupHostZonesBeforeUpgrade() checkRateLimitsAfterUpgrade := s.SetupRateLimitsBeforeUpgrade() checkCommunityPoolTaxAfterUpgrade := s.SetupCommunityPoolTaxBeforeUpgrade() checkQueriesAfterUpgrade := s.SetupQueriesBeforeUpgrade() + checkDisableTokenizationICASubmitted := s.SetupTestDisableTokenization(channelId, portId) checkProp225AfterUpgrade := s.SetupProp225BeforeUpgrade() // Submit upgrade and confirm handler succeeds @@ -84,6 +89,7 @@ func (s *UpgradeTestSuite) TestUpgrade() { checkRateLimitsAfterUpgrade() checkCommunityPoolTaxAfterUpgrade() checkQueriesAfterUpgrade() + checkDisableTokenizationICASubmitted() checkProp225AfterUpgrade() } @@ -126,9 +132,9 @@ func (s *UpgradeTestSuite) checkCommunityPoolICAAccountsRegistered(chainId strin func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { hostZones := []stakeibctypes.HostZone{ { - ChainId: GaiaChainId, + ChainId: v17.GaiaChainId, HostDenom: Atom, - ConnectionId: "connection-1", + ConnectionId: ibctesting.FirstConnectionID, // must be connection-0 since an ICA will be submitted Validators: []*stakeibctypes.Validator{ {Address: "val1", SlashQueryInProgress: false}, {Address: "val2", SlashQueryInProgress: true}, @@ -169,14 +175,14 @@ func (s *UpgradeTestSuite) SetupHostZonesBeforeUpgrade() func() { // Return callback to check store after upgrade return func() { // Check that the module and ICA accounts were registered - s.checkCommunityPoolModuleAccountsRegistered(GaiaChainId) + s.checkCommunityPoolModuleAccountsRegistered(v17.GaiaChainId) s.checkCommunityPoolModuleAccountsRegistered(v17.OsmosisChainId) - s.checkCommunityPoolICAAccountsRegistered(GaiaChainId) + s.checkCommunityPoolICAAccountsRegistered(v17.GaiaChainId) s.checkCommunityPoolICAAccountsRegistered(v17.OsmosisChainId) // Check that the redemption rate bounds were set - gaiaHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, GaiaChainId) + gaiaHostZone, found := s.App.StakeibcKeeper.GetHostZone(s.Ctx, v17.GaiaChainId) s.Require().True(found) s.Require().Equal(sdk.MustNewDecFromStr("1.045"), gaiaHostZone.MinRedemptionRate, "gaia min outer") // 1.1 - 5% = 1.045 @@ -274,7 +280,7 @@ func (s *UpgradeTestSuite) SetupRateLimitsBeforeUpgrade() func() { stAtomToGaiaRateLimit, found := s.App.RatelimitKeeper.GetRateLimit(s.Ctx, StAtom, gaiaChannelId) s.Require().True(found) - atomThreshold := v17.UpdatedRateLimits[GaiaChainId] + atomThreshold := v17.UpdatedRateLimits[v17.GaiaChainId] s.Require().Equal(atomThreshold, stAtomToGaiaRateLimit.Quota.MaxPercentSend, "statom -> gaia max percent send") s.Require().Equal(atomThreshold, stAtomToGaiaRateLimit.Quota.MaxPercentRecv, "statom -> gaia max percent recv") s.Require().Equal(initialFlow, stAtomToGaiaRateLimit.Flow.Outflow, "statom -> gaia outflow") @@ -344,6 +350,17 @@ func (s *UpgradeTestSuite) SetupQueriesBeforeUpgrade() func() { } } +func (s *UpgradeTestSuite) SetupTestDisableTokenization(channelId, portId string) func() { + // Get the current sequence number (to check that it incremented) + startSequence := s.MustGetNextSequenceNumber(portId, channelId) + + // Return callback to that the ICA was submitted + return func() { + endSequence := s.MustGetNextSequenceNumber(portId, channelId) + s.Require().Equal(startSequence+1, endSequence, "sequence number should have incremented from disabling detokenization") + } +} + func (s *UpgradeTestSuite) SetupProp225BeforeUpgrade() func() { // Grab the community pool growth address and balance communityPoolGrowthAddress := sdk.MustAccAddressFromBech32(v17.CommunityPoolGrowthAddress) @@ -725,3 +742,19 @@ func (s *UpgradeTestSuite) TestAddRateLimitToOsmosis() { err = v17.AddRateLimitToOsmosis(s.Ctx, s.App.RatelimitKeeper) s.Require().ErrorContains(err, "channel value is zero") } + +func (s *UpgradeTestSuite) TestDisableTokenization() { + // Create the host zone and delegation channel + owner := stakeibctypes.FormatHostZoneICAOwner(v17.GaiaChainId, stakeibctypes.ICAAccountType_DELEGATION) + channelId, portId := s.CreateICAChannel(owner) + + s.App.StakeibcKeeper.SetHostZone(s.Ctx, stakeibctypes.HostZone{ + ChainId: v17.GaiaChainId, + ConnectionId: ibctesting.FirstConnectionID, + }) + + // Call the disable function and confirm the sequence number incremented (indicating an ICA was submitted) + s.CheckICATxSubmitted(portId, channelId, func() error { + return v17.DisableTokenization(s.Ctx, s.App.StakeibcKeeper, v17.GaiaChainId) + }) +} diff --git a/proto/cosmos/staking/v1beta1/lsm_tx.proto b/proto/cosmos/staking/v1beta1/lsm_tx.proto index 239a11ebf6..3d35408192 100644 --- a/proto/cosmos/staking/v1beta1/lsm_tx.proto +++ b/proto/cosmos/staking/v1beta1/lsm_tx.proto @@ -28,3 +28,12 @@ message MsgRedeemTokensForShares { message MsgRedeemTokensForSharesResponse { cosmos.base.v1beta1.Coin amount = 1 [ (gogoproto.nullable) = false ]; } + +// MsgDisableTokenizeShares prevents LSM tokenization of shares for address +message MsgDisableTokenizeShares { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + string delegator_address = 1 + [ (gogoproto.moretags) = "yaml:\"delegator_address\"" ]; +} diff --git a/x/stakeibc/types/lsm_tx.pb.go b/x/stakeibc/types/lsm_tx.pb.go index e6c3eb3a73..f824d5172a 100644 --- a/x/stakeibc/types/lsm_tx.pb.go +++ b/x/stakeibc/types/lsm_tx.pb.go @@ -110,9 +110,48 @@ func (m *MsgRedeemTokensForSharesResponse) GetAmount() types.Coin { return types.Coin{} } +// MsgDisableTokenizeShares prevents LSM tokenization of shares for address +type MsgDisableTokenizeShares struct { + DelegatorAddress string `protobuf:"bytes,1,opt,name=delegator_address,json=delegatorAddress,proto3" json:"delegator_address,omitempty" yaml:"delegator_address"` +} + +func (m *MsgDisableTokenizeShares) Reset() { *m = MsgDisableTokenizeShares{} } +func (m *MsgDisableTokenizeShares) String() string { return proto.CompactTextString(m) } +func (*MsgDisableTokenizeShares) ProtoMessage() {} +func (*MsgDisableTokenizeShares) Descriptor() ([]byte, []int) { + return fileDescriptor_34c3b474a863e424, []int{2} +} +func (m *MsgDisableTokenizeShares) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgDisableTokenizeShares) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgDisableTokenizeShares.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgDisableTokenizeShares) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgDisableTokenizeShares.Merge(m, src) +} +func (m *MsgDisableTokenizeShares) XXX_Size() int { + return m.Size() +} +func (m *MsgDisableTokenizeShares) XXX_DiscardUnknown() { + xxx_messageInfo_MsgDisableTokenizeShares.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgDisableTokenizeShares proto.InternalMessageInfo + func init() { proto.RegisterType((*MsgRedeemTokensForShares)(nil), "cosmos.staking.v1beta1.MsgRedeemTokensForShares") proto.RegisterType((*MsgRedeemTokensForSharesResponse)(nil), "cosmos.staking.v1beta1.MsgRedeemTokensForSharesResponse") + proto.RegisterType((*MsgDisableTokenizeShares)(nil), "cosmos.staking.v1beta1.MsgDisableTokenizeShares") } func init() { @@ -120,7 +159,7 @@ func init() { } var fileDescriptor_34c3b474a863e424 = []byte{ - // 327 bytes of a gzipped FileDescriptorProto + // 349 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4e, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0xd6, 0x2f, 0x2e, 0x49, 0xcc, 0xce, 0xcc, 0x4b, 0xd7, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0xcf, 0x29, 0xce, 0x8d, 0x2f, 0xa9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, @@ -135,13 +174,14 @@ var fileDescriptor_34c3b474a863e424 = []byte{ 0xa4, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa9, 0x07, 0xf5, 0x06, 0xc8, 0x61, 0x30, 0x3f, 0xe8, 0x39, 0xe7, 0x67, 0xe6, 0x39, 0xb1, 0x9c, 0xb8, 0x27, 0xcf, 0x10, 0x04, 0x55, 0x6e, 0xc5, 0xd1, 0xb1, 0x40, 0x9e, 0xe1, 0xc5, 0x02, 0x79, 0x06, 0xa5, 0x68, 0x2e, 0x05, 0x5c, 0x2e, 0x0d, 0x4a, 0x2d, - 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x45, 0xb2, 0x86, 0x91, 0x24, 0x6b, 0x9c, 0x7c, 0x4e, 0x3c, 0x92, - 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, - 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0xca, 0x28, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, - 0x39, 0x3f, 0x57, 0x3f, 0xb8, 0xa4, 0x28, 0x33, 0x25, 0x55, 0xd7, 0x27, 0x31, 0x09, 0x14, 0x49, - 0x20, 0xb6, 0x7e, 0x99, 0xa1, 0x99, 0x7e, 0x05, 0x38, 0xc6, 0x52, 0x33, 0x93, 0x92, 0xf5, 0x4b, - 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0x81, 0x6b, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x49, - 0x5c, 0x2e, 0x7e, 0xd1, 0x01, 0x00, 0x00, + 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x45, 0xb2, 0x86, 0x91, 0x24, 0x6b, 0x94, 0xf2, 0xc1, 0xc1, 0xe0, + 0x92, 0x59, 0x9c, 0x98, 0x94, 0x93, 0x0a, 0x36, 0x3d, 0xb3, 0x2a, 0x95, 0xea, 0xc1, 0x80, 0xf0, + 0x8d, 0x93, 0xcf, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, + 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x19, 0xa5, 0x67, + 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x07, 0x97, 0x14, 0x65, 0xa6, 0xa4, 0xea, + 0xfa, 0x24, 0x26, 0x81, 0x52, 0x05, 0x88, 0xad, 0x5f, 0x66, 0x68, 0xa6, 0x5f, 0x01, 0x4e, 0x22, + 0xa9, 0x99, 0x49, 0xc9, 0xfa, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0xe0, 0xd8, 0x34, 0x06, + 0x04, 0x00, 0x00, 0xff, 0xff, 0x8a, 0x66, 0xa5, 0x97, 0x42, 0x02, 0x00, 0x00, } func (m *MsgRedeemTokensForShares) Marshal() (dAtA []byte, err error) { @@ -217,6 +257,36 @@ func (m *MsgRedeemTokensForSharesResponse) MarshalToSizedBuffer(dAtA []byte) (in return len(dAtA) - i, nil } +func (m *MsgDisableTokenizeShares) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgDisableTokenizeShares) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgDisableTokenizeShares) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.DelegatorAddress) > 0 { + i -= len(m.DelegatorAddress) + copy(dAtA[i:], m.DelegatorAddress) + i = encodeVarintLsmTx(dAtA, i, uint64(len(m.DelegatorAddress))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintLsmTx(dAtA []byte, offset int, v uint64) int { offset -= sovLsmTx(v) base := offset @@ -254,6 +324,19 @@ func (m *MsgRedeemTokensForSharesResponse) Size() (n int) { return n } +func (m *MsgDisableTokenizeShares) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.DelegatorAddress) + if l > 0 { + n += 1 + l + sovLsmTx(uint64(l)) + } + return n +} + func sovLsmTx(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -458,6 +541,88 @@ func (m *MsgRedeemTokensForSharesResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *MsgDisableTokenizeShares) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLsmTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgDisableTokenizeShares: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgDisableTokenizeShares: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DelegatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLsmTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLsmTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLsmTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DelegatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipLsmTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthLsmTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipLsmTx(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0